diff --git a/Hyperbee.Json.sln.DotSettings b/Hyperbee.Json.sln.DotSettings index 3c37ed14..83de03a7 100644 --- a/Hyperbee.Json.sln.DotSettings +++ b/Hyperbee.Json.sln.DotSettings @@ -53,11 +53,13 @@ True True True + True True True True True True + True True True True @@ -69,6 +71,7 @@ True True True + True True True True @@ -91,6 +94,7 @@ True True True + True True True True diff --git a/README.md b/README.md index 613fd1d3..fa7df9c7 100644 --- a/README.md +++ b/README.md @@ -14,10 +14,10 @@ The library is designed to be quick and extensible, allowing support for other J ## JSONPath Consensus -Hyperbee.Json aims to follow the emerging [JSONPath consensus](https://cburgmer.github.io/json-path-comparison) standard where possible. -This standardization effort is critical for ensuring consistent behavior across different implementations of JSONPath. -However, 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 keeping an eye on standardization progress. +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 +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. ## Installation @@ -112,6 +112,31 @@ foreach (var item in result) } ``` +#### Working with (JsonElement, Path) pairs +```csharp +using Hyperbee.JsonPath; +using System.Text.Json; + +var json = """ +{ + "store": { + "book": [ + { "category": "fiction" }, + { "category": "science" } + ] + } +} +"""; + +var root = JsonDocument.Parse(json); +var result = JsonPath.SelectPath(root, "$.store.book[0].category"); + +var (node, path) = result.First(); + +Console.WriteLine(node); // Output: "fiction" +Console.WriteLine(path); // Output: "$.store.book[0].category +``` + #### Working with JsonNode ```csharp @@ -317,37 +342,39 @@ Here is a performance comparison of various queries on the standard book store d ``` ``` -| Method | Filter | Mean | Error | StdDev | Allocated -|:----------------------- |:-------------------------------- |:--------- |:---------- |:--------- |:--------- -| Hyperbee_JsonElement | $..* `First()` | 3.042 us | 0.3928 us | 0.0215 us | 3.82 KB -| JsonEverything_JsonNode | $..* `First()` | 3.201 us | 0.9936 us | 0.0545 us | 3.53 KB -| Hyperbee_JsonNode | $..* `First()` | 3.206 us | 1.8335 us | 0.1005 us | 3.11 KB -| JsonCons_JsonElement | $..* `First()` | 5.666 us | 0.7342 us | 0.0402 us | 8.48 KB -| Newtonsoft_JObject | $..* `First()` | 8.741 us | 1.7537 us | 0.0961 us | 14.22 KB -| | | | | | -| JsonCons_JsonElement | $..* | 5.599 us | 1.1146 us | 0.0611 us | 8.45 KB -| Hyperbee_JsonElement | $..* | 9.511 us | 0.6130 us | 0.0336 us | 13.97 KB -| Newtonsoft_JObject | $..* | 10.082 us | 1.0318 us | 0.0566 us | 14.86 KB -| Hyperbee_JsonNode | $..* | 12.051 us | 5.3268 us | 0.2920 us | 13.92 KB -| JsonEverything_JsonNode | $..* | 22.612 us | 16.0118 us | 0.8777 us | 36.81 KB -| | | | | | -| Hyperbee_JsonElement | $..price | 4.930 us | 3.3771 us | 0.1851 us | 6.58 KB -| JsonCons_JsonElement | $..price | 4.934 us | 1.0796 us | 0.0592 us | 5.65 KB -| Hyperbee_JsonNode | $..price | 7.784 us | 1.7326 us | 0.0950 us | 9.13 KB -| Newtonsoft_JObject | $..price | 9.913 us | 2.6681 us | 0.1462 us | 14.4 KB -| JsonEverything_JsonNode | $..price | 16.365 us | 4.0688 us | 0.2230 us | 27.63 KB -| | | | | | -| Hyperbee_JsonElement | $.store.book[?(@.price == 8.99)] | 4.062 us | 0.2682 us | 0.0147 us | 6.08 KB -| JsonCons_JsonElement | $.store.book[?(@.price == 8.99)] | 4.959 us | 0.5051 us | 0.0277 us | 5.05 KB -| Hyperbee_JsonNode | $.store.book[?(@.price == 8.99)] | 6.775 us | 1.3945 us | 0.0764 us | 8.34 KB -| Newtonsoft_JObject | $.store.book[?(@.price == 8.99)] | 10.050 us | 5.3711 us | 0.2944 us | 15.84 KB -| JsonEverything_JsonNode | $.store.book[?(@.price == 8.99)] | 11.223 us | 0.5535 us | 0.0303 us | 15.85 KB -| | | | | | -| Hyperbee_JsonElement | $.store.book[0] | 2.812 us | 0.5097 us | 0.0279 us | 2.81 KB -| Hyperbee_JsonNode | $.store.book[0] | 3.259 us | 0.1929 us | 0.0106 us | 3.12 KB -| JsonCons_JsonElement | $.store.book[0] | 3.365 us | 10.9259 us | 0.5989 us | 3.21 KB -| JsonEverything_JsonNode | $.store.book[0] | 4.670 us | 0.6449 us | 0.0354 us | 5.96 KB -| Newtonsoft_JObject | $.store.book[0] | 8.572 us | 1.5455 us | 0.0847 us | 14.56 KB +| 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 +``` +``` ``` ## Additional Documentation diff --git a/docs/ADDITIONAL-CLASSES.md b/docs/ADDITIONAL-CLASSES.md index 6246a912..73d99814 100644 --- a/docs/ADDITIONAL-CLASSES.md +++ b/docs/ADDITIONAL-CLASSES.md @@ -1,56 +1,21 @@ ## Additional Classes -In addition to JSONPath processing, a few additional classes are provided to support dynamic property access, -property diving, and element comparisons. - -### 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. - -#### DynamicJsonConverter - -```csharp -var serializerOptions = new JsonSerializerOptions -{ - Converters = {new DynamicJsonConverter()} -}; - -// jsonInput is a string containing the bookstore json from the previous examples -var jobject = JsonSerializer.Deserialize( jsonInput, serializerOptions); - -Assert.IsTrue( jobject.store.bicycle.color == "red" ); - -var jsonOutput = JsonSerializer.Serialize( jobject, serializerOptions ) as string; - -Assert.IsTrue( jsonInput == jsonOutput ); -``` - -##### Enum handling - -When deserializing, the converter will treat enumerations as strings. You can override this behavior by setting -the `TryReadValueHandler` on the converter. This handler will allow you to intercept and convert string and -numeric values during the deserialization process. - -### Equality Helpers - -| Method | Description -|:-----------------------------------|:----------- -| `JsonElement.DeepEquals` | Performs a deep equals comparison on two `JsonElements` -| `JsonElementEqualityDeepComparer` | A deep equals equality comparer that compares two `JsonElements` +In addition to JSONPath, a few additional classes are provided to support pointer-style +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 expects simplified JSON Path notation. +Unlike JSON Pointer, property diving notation expects a singular JSON Path. | Method | Description |:-----------------------------------|:----------- -| `JsonElement.GetPropertyFromPath` | Dives for properties using absolute locations like `$['store']['book'][2]['author']` +| `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 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: @@ -65,9 +30,46 @@ Examples of valid path syntax: ### JsonElement Path -Unlike `JsonNode`, `JsonElement` does not have a `Path` property. `JsonPathResolver` will find the path +Unlike `JsonNode`, `JsonElement` does not have a `Path` property. `JsonPathBuilder` will find the path for a given `JsonElement`. | Method | Description |:---------------------------|:----------- -| `JsonPathResolver.GetPath` | Returns the JsonPath location string for a given element +| `JsonPathBuilder.GetPath` | Returns the JsonPath location string for a given element + +### Equality Helpers + +| Method | Description +|:-----------------------------------|:----------- +| `JsonElement.DeepEquals` | Performs a deep equals comparison on two `JsonElements` +| `JsonElementDeepEqualityComparer` | A deep equals equality comparer that compares two `JsonElements` + +### 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. + +#### DynamicJsonConverter + +```csharp +var serializerOptions = new JsonSerializerOptions +{ + Converters = {new DynamicJsonConverter()} +}; + +// jsonInput is a string containing the bookstore json from the previous examples +var jobject = JsonSerializer.Deserialize( jsonInput, serializerOptions); + +Assert.IsTrue( jobject.store.bicycle.color == "red" ); + +var jsonOutput = JsonSerializer.Serialize( jobject, serializerOptions ) as string; + +Assert.IsTrue( jsonInput == jsonOutput ); +``` + +##### Enum handling + +When deserializing, the converter will treat enumerations as strings. You can override this behavior by setting +the `TryReadValueHandler` on the converter. This handler will allow you to intercept and convert string and +numeric values during the deserialization process. diff --git a/docs/JSONPATH-SYNTAX.md b/docs/JSONPATH-SYNTAX.md index 0ab67376..8d73add8 100644 --- a/docs/JSONPATH-SYNTAX.md +++ b/docs/JSONPATH-SYNTAX.md @@ -1,17 +1,22 @@ # JSONPath Syntax Reference -JSONPath is a query language for JSON, similar to XPath for XML. It allows you to extract specific values from JSON documents. -This page outlines the syntax and operators supported by Hyperbee.Json. +JSONPath is a query language for JSON that allows you to extract specific values from JSON documents. +This page outlines the syntax and operators supported by `Hyperbee.Json`. -## Basic Syntax +## Basic Syntax and Operators ### Root Node -`$` Refers to the root object or array. +`$` is used as the root node identifier. $ refers to the entire JSON document, serving as the starting +point for any JSONPath expression. + +For instance, the expression `$.store.book` would navigate from the root of the JSON document to the +store object and then to the book array within that object. ### Child Operator -`.` Access Child Member. +`.` is used to select the child elements of a given node. It helps to navigate through the JSON +structure by accessing the properties of objects and elements of arrays directly from their parent nodes. | Expression | Description |------------------------|------------------------------------------------------- @@ -476,4 +481,3 @@ Filters can be combined using logical operators `&&` (and) and `||` (or). - Stefan Goessner for the [original JSONPath implementation](https://goessner.net/articles/JsonPath/). - JSONPath Specification [RFC 9535](https://www.rfc-editor.org/rfc/rfc9535.html). - diff --git a/src/Hyperbee.Json/Descriptors/Element/ElementValueAccessor.cs b/src/Hyperbee.Json/Descriptors/Element/ElementValueAccessor.cs index 29cf766d..6de13d15 100644 --- a/src/Hyperbee.Json/Descriptors/Element/ElementValueAccessor.cs +++ b/src/Hyperbee.Json/Descriptors/Element/ElementValueAccessor.cs @@ -1,7 +1,8 @@ using System.Globalization; using System.Runtime.CompilerServices; +using System.Text; using System.Text.Json; -using Hyperbee.Json.Descriptors.Element.Functions; +using Hyperbee.Json.Extensions; namespace Hyperbee.Json.Descriptors.Element; @@ -48,41 +49,35 @@ public JsonElement GetElementAt( in JsonElement value, int index ) } [MethodImpl( MethodImplOptions.AggressiveInlining )] - public bool IsObjectOrArray( in JsonElement value ) + public NodeKind GetNodeKind( in JsonElement value ) { - return value.ValueKind is JsonValueKind.Array or JsonValueKind.Object; - } - - [MethodImpl( MethodImplOptions.AggressiveInlining )] - public bool IsArray( in JsonElement value, out int length ) - { - if ( value.ValueKind == JsonValueKind.Array ) + return value.ValueKind switch { - length = value.GetArrayLength(); - return true; - } - - length = 0; - return false; + JsonValueKind.Object => NodeKind.Object, + JsonValueKind.Array => NodeKind.Array, + _ => NodeKind.Value + }; } [MethodImpl( MethodImplOptions.AggressiveInlining )] - public bool IsObject( in JsonElement value ) + public int GetArrayLength( in JsonElement value ) { - return value.ValueKind is JsonValueKind.Object; + return value.ValueKind == JsonValueKind.Array + ? value.GetArrayLength() + : 0; } - public bool TryGetChildValue( in JsonElement value, string childKey, out JsonElement childValue ) + public bool TryGetChildValue( in JsonElement value, string childSelector, out JsonElement childValue ) { switch ( value.ValueKind ) { case JsonValueKind.Object: - if ( value.TryGetProperty( childKey, out childValue ) ) + if ( value.TryGetProperty( childSelector, out childValue ) ) return true; break; case JsonValueKind.Array: - if ( int.TryParse( childKey, NumberStyles.Integer, CultureInfo.InvariantCulture, out var index ) ) + if ( int.TryParse( childSelector, NumberStyles.Integer, CultureInfo.InvariantCulture, out var index ) ) { if ( index >= 0 && index < value.GetArrayLength() ) { @@ -94,8 +89,8 @@ public bool TryGetChildValue( in JsonElement value, string childKey, out JsonEle break; default: - if ( !IsPathOperator( childKey ) ) - throw new ArgumentException( $"Invalid child type '{childKey}'. Expected child to be Object, Array or a path selector.", nameof( value ) ); + if ( !IsPathOperator( childSelector ) ) + throw new ArgumentException( $"Invalid child type '{childSelector}'. Expected child to be Object, Array or a path selector.", nameof( value ) ); break; } @@ -115,8 +110,57 @@ static bool IsPathOperator( ReadOnlySpan x ) } } - public object GetAsValue( IEnumerable elements ) + public bool DeepEquals( JsonElement left, JsonElement right ) + { + return left.DeepEquals( right ); + } + + public bool TryParseNode( ReadOnlySpan item, out JsonElement element ) + { + var bytes = Encoding.UTF8.GetBytes( item.ToArray() ); + var reader = new Utf8JsonReader( bytes ); + + try + { + if ( JsonDocument.TryParseValue( ref reader, out var document ) ) + { + element = document.RootElement; + return true; + } + } + catch + { + // ignored: fall through + } + + element = default; + return false; + } + + public bool TryGetValueFromNode( JsonElement element, out object value ) { - return ValueElementFunction.Value( elements ); + switch ( element.ValueKind ) + { + case JsonValueKind.String: + value = element.GetString(); + break; + case JsonValueKind.Number: + value = element.GetSingle(); + break; + case JsonValueKind.True: + value = true; + break; + case JsonValueKind.False: + value = false; + break; + case JsonValueKind.Null: + value = null; + break; + default: + value = false; + return false; + } + + return true; } } diff --git a/src/Hyperbee.Json/Descriptors/IValueAccessor.cs b/src/Hyperbee.Json/Descriptors/IValueAccessor.cs index a3fe4c72..2898343f 100644 --- a/src/Hyperbee.Json/Descriptors/IValueAccessor.cs +++ b/src/Hyperbee.Json/Descriptors/IValueAccessor.cs @@ -4,9 +4,10 @@ public interface IValueAccessor { IEnumerable<(TNode, string, SelectorKind)> EnumerateChildren( TNode value, bool includeValues = true ); TNode GetElementAt( in TNode value, int index ); - bool IsObjectOrArray( in TNode value ); - bool IsArray( in TNode value, out int length ); - bool IsObject( in TNode value ); - bool TryGetChildValue( in TNode value, string childKey, out TNode childValue ); - object GetAsValue( IEnumerable elements ); + NodeKind GetNodeKind( in TNode value ); + int GetArrayLength( in TNode value ); + bool TryGetChildValue( in TNode value, string childSelector, out TNode childValue ); + bool TryParseNode( ReadOnlySpan item, out TNode value ); + bool DeepEquals( TNode left, TNode right ); + bool TryGetValueFromNode( TNode item, out object o ); } diff --git a/src/Hyperbee.Json/Descriptors/Node/NodeValueAccessor.cs b/src/Hyperbee.Json/Descriptors/Node/NodeValueAccessor.cs index 907a5770..9b50425f 100644 --- a/src/Hyperbee.Json/Descriptors/Node/NodeValueAccessor.cs +++ b/src/Hyperbee.Json/Descriptors/Node/NodeValueAccessor.cs @@ -2,8 +2,6 @@ using System.Runtime.CompilerServices; using System.Text.Json; using System.Text.Json.Nodes; -using Hyperbee.Json.Descriptors.Node.Functions; -using Hyperbee.Json.Extensions; namespace Hyperbee.Json.Descriptors.Node; @@ -48,44 +46,39 @@ public JsonNode GetElementAt( in JsonNode value, int index ) } [MethodImpl( MethodImplOptions.AggressiveInlining )] - public bool IsObjectOrArray( in JsonNode value ) + public NodeKind GetNodeKind( in JsonNode value ) { - return value is JsonObject or JsonArray; + return value switch + { + JsonArray => NodeKind.Array, + JsonObject => NodeKind.Object, + _ => NodeKind.Value + }; } [MethodImpl( MethodImplOptions.AggressiveInlining )] - public bool IsArray( in JsonNode value, out int length ) + public int GetArrayLength( in JsonNode value ) { if ( value is JsonArray jsonArray ) - { - length = jsonArray.Count; - return true; - } + return jsonArray.Count; - length = 0; - return false; + return 0; } - [MethodImpl( MethodImplOptions.AggressiveInlining )] - public bool IsObject( in JsonNode value ) - { - return value is JsonObject; - } - - public bool TryGetChildValue( in JsonNode value, string childKey, out JsonNode childValue ) + public bool TryGetChildValue( in JsonNode value, string childSelector, out JsonNode childValue ) { switch ( value ) { case JsonObject valueObject: { - if ( valueObject.TryGetPropertyValue( childKey, out childValue ) ) + if ( valueObject.TryGetPropertyValue( childSelector, out childValue ) ) return true; break; } case JsonArray valueArray: { - if ( int.TryParse( childKey, NumberStyles.Integer, CultureInfo.InvariantCulture, out var index ) ) + if ( int.TryParse( childSelector, NumberStyles.Integer, CultureInfo.InvariantCulture, out var index ) ) { if ( index >= 0 && index < valueArray.Count ) { @@ -98,8 +91,8 @@ public bool TryGetChildValue( in JsonNode value, string childKey, out JsonNode c } default: { - if ( !IsPathOperator( childKey ) ) - throw new ArgumentException( $"Invalid child type '{childKey}'. Expected child to be Object, Array or a path selector.", nameof( value ) ); + if ( !IsPathOperator( childSelector ) ) + throw new ArgumentException( $"Invalid child type '{childSelector}'. Expected child to be Object, Array or a path selector.", nameof( value ) ); break; } @@ -121,8 +114,51 @@ static bool IsPathOperator( ReadOnlySpan x ) } } - public object GetAsValue( IEnumerable nodes ) + public bool DeepEquals( JsonNode left, JsonNode right ) + { + return JsonNode.DeepEquals( left, right ); + } + + + public bool TryParseNode( ReadOnlySpan item, out JsonNode node ) + { + try + { + node = JsonNode.Parse( item.ToString() ); + return true; + } + catch + { + node = null; + return false; + } + } + + public bool TryGetValueFromNode( JsonNode node, out object value ) { - return ValueNodeFunction.Value( nodes ); + switch ( node?.GetValueKind() ) + { + case JsonValueKind.String: + value = node.GetValue(); + break; + case JsonValueKind.Number: + value = node.GetValue(); + break; + case JsonValueKind.True: + value = true; + break; + case JsonValueKind.False: + value = false; + break; + case JsonValueKind.Null: + case null: + value = null; + break; + default: + value = false; + return false; + } + + return true; } } diff --git a/src/Hyperbee.Json/Descriptors/NodeKind.cs b/src/Hyperbee.Json/Descriptors/NodeKind.cs new file mode 100644 index 00000000..eecf08f9 --- /dev/null +++ b/src/Hyperbee.Json/Descriptors/NodeKind.cs @@ -0,0 +1,8 @@ +namespace Hyperbee.Json.Descriptors; + +public enum NodeKind +{ + Object, + Array, + Value +} diff --git a/src/Hyperbee.Json/Extensions/JsonElementExtensions.cs b/src/Hyperbee.Json/Extensions/JsonElementExtensions.cs index 9094e86d..536fc29d 100644 --- a/src/Hyperbee.Json/Extensions/JsonElementExtensions.cs +++ b/src/Hyperbee.Json/Extensions/JsonElementExtensions.cs @@ -47,7 +47,7 @@ public static T ToObject( this JsonElement value, JsonSerializerOptions optio if ( strB == null ) return false; - var comparer = new JsonElementDeepEqualsComparer( options.MaxDepth ); + var comparer = new JsonElementDeepEqualityComparer( options.MaxDepth ); using var docB = JsonDocument.Parse( strB, options ); return comparer.Equals( elmA, docB.RootElement ); @@ -55,7 +55,7 @@ public static T ToObject( this JsonElement value, JsonSerializerOptions optio public static bool DeepEquals( this JsonElement elmA, JsonElement elmB, JsonDocumentOptions options = default ) { - var comparer = new JsonElementDeepEqualsComparer( options.MaxDepth ); + var comparer = new JsonElementDeepEqualityComparer( options.MaxDepth ); return comparer.Equals( elmA, elmB ); } diff --git a/src/Hyperbee.Json/Extensions/JsonHelper.cs b/src/Hyperbee.Json/Extensions/JsonPathHelper.cs similarity index 97% rename from src/Hyperbee.Json/Extensions/JsonHelper.cs rename to src/Hyperbee.Json/Extensions/JsonPathHelper.cs index 1995411c..d896634e 100644 --- a/src/Hyperbee.Json/Extensions/JsonHelper.cs +++ b/src/Hyperbee.Json/Extensions/JsonPathHelper.cs @@ -2,7 +2,7 @@ namespace Hyperbee.Json.Extensions; -public static class JsonHelper +public static class JsonPathHelper { // conversion diff --git a/src/Hyperbee.Json/Extensions/JsonPropertyFromPathExtensions.cs b/src/Hyperbee.Json/Extensions/JsonPathPointerExtensions.cs similarity index 89% rename from src/Hyperbee.Json/Extensions/JsonPropertyFromPathExtensions.cs rename to src/Hyperbee.Json/Extensions/JsonPathPointerExtensions.cs index a9a9b69f..7e897173 100644 --- a/src/Hyperbee.Json/Extensions/JsonPropertyFromPathExtensions.cs +++ b/src/Hyperbee.Json/Extensions/JsonPathPointerExtensions.cs @@ -4,11 +4,11 @@ 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 uses JsonPath notation. +// absolute singular paths. similar to JsonPointer but using JsonPath notation. // // syntax supports singular 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. +// Json path style wildcard '*', '..', and '[a,b]' multi-result selector notations are NOT supported. // // examples: // prop1.prop2 @@ -18,14 +18,14 @@ namespace Hyperbee.Json.Extensions; // prop1['prop.2'] // prop1.'prop.2'[0].prop3 -public static class JsonPropertyFromPathExtensions +public static class JsonPathPointerExtensions { - public static JsonElement GetPropertyFromPath( this JsonElement jsonElement, ReadOnlySpan propertyPath ) + public static JsonElement FromJsonPathPointer( this JsonElement jsonElement, ReadOnlySpan pointer ) { - if ( IsNullOrUndefined( jsonElement ) || propertyPath.IsEmpty ) + if ( IsNullOrUndefined( jsonElement ) || pointer.IsEmpty ) return default; - var splitter = new JsonPropertyPathSplitter( propertyPath ); + var splitter = new JsonPathPointerSplitter( pointer ); while ( splitter.TryMoveNext( out var name ) ) { @@ -46,12 +46,12 @@ public static JsonElement GetPropertyFromPath( this JsonElement jsonElement, Rea static bool IsNullOrUndefined( JsonElement value ) => value.ValueKind is JsonValueKind.Null or JsonValueKind.Undefined; } - public static JsonNode GetPropertyFromPath( this JsonNode jsonNode, ReadOnlySpan propertyPath ) + public static JsonNode FromJsonPathPointer( this JsonNode jsonNode, ReadOnlySpan pointer ) { - if ( jsonNode == null || propertyPath.IsEmpty ) + if ( jsonNode == null || pointer.IsEmpty ) return default; - var splitter = new JsonPropertyPathSplitter( propertyPath ); + var splitter = new JsonPathPointerSplitter( pointer ); while ( splitter.TryMoveNext( out var name ) ) { @@ -74,7 +74,7 @@ public static JsonNode GetPropertyFromPath( this JsonNode jsonNode, ReadOnlySpan return jsonNode; } - private ref struct JsonPropertyPathSplitter //TODO Support escaping of \' and bracket counting in literals. Add to unit tests. + 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 @@ -106,7 +106,7 @@ private enum BracketContent Number } - internal JsonPropertyPathSplitter( ReadOnlySpan span ) + internal JsonPathPointerSplitter( ReadOnlySpan span ) { if ( span.StartsWith( "$." ) ) span = span[2..]; diff --git a/src/Hyperbee.Json/Extensions/JsonPathSelectExtensions.cs b/src/Hyperbee.Json/Extensions/JsonPathSelectExtensions.cs index 4c292887..da73a93e 100644 --- a/src/Hyperbee.Json/Extensions/JsonPathSelectExtensions.cs +++ b/src/Hyperbee.Json/Extensions/JsonPathSelectExtensions.cs @@ -5,6 +5,11 @@ namespace Hyperbee.Json.Extensions; public static class JsonPathSelectExtensions { + public static IEnumerable Select( this JsonNode node, string query ) + { + return JsonPath.Select( node, query ); + } + public static IEnumerable Select( this JsonElement element, string query ) { return JsonPath.Select( element, query ); @@ -15,8 +20,25 @@ public static IEnumerable Select( this JsonDocument document, strin return JsonPath.Select( document.RootElement, query ); } - public static IEnumerable Select( this JsonNode node, string query ) + public static IEnumerable<(JsonElement Node, string Path)> SelectPath( this JsonDocument document, string query ) { - return JsonPath.Select( node, query ); + return document.RootElement.SelectPath( query ); + } + + public static IEnumerable<(JsonElement Node, string Path)> SelectPath( this JsonElement element, string query ) + { + var pathBuilder = new JsonPathBuilder( element ); + + foreach ( var result in JsonPath.Select( element, query, NodeProcessor ) ) + { + yield return (result, pathBuilder.GetPath( result )); + } + + yield break; + + void NodeProcessor( in JsonElement parent, in JsonElement value, string key, in JsonPathSegment segment ) + { + pathBuilder.InsertItem( parent, value, key ); // seed the path builder with the parent and value + } } } diff --git a/src/Hyperbee.Json/Filters/Parser/Expressions/ComparerExpressionFactory.cs b/src/Hyperbee.Json/Filters/Parser/Expressions/ComparerExpressionFactory.cs new file mode 100644 index 00000000..e296e135 --- /dev/null +++ b/src/Hyperbee.Json/Filters/Parser/Expressions/ComparerExpressionFactory.cs @@ -0,0 +1,246 @@ +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/JsonExpressionFactory.cs b/src/Hyperbee.Json/Filters/Parser/Expressions/JsonExpressionFactory.cs new file mode 100644 index 00000000..9584ce16 --- /dev/null +++ b/src/Hyperbee.Json/Filters/Parser/Expressions/JsonExpressionFactory.cs @@ -0,0 +1,18 @@ +using System.Linq.Expressions; + +namespace Hyperbee.Json.Filters.Parser.Expressions; + +internal class JsonExpressionFactory : IExpressionFactory +{ + public static bool TryGetExpression( ref ParserState state, out Expression expression, FilterContext context ) + { + if ( context.Descriptor.Accessor.TryParseNode( state.Item.ToString(), out var node ) ) + { + expression = Expression.Constant( new[] { node } ); + return true; + } + + expression = null; + return false; + } +} diff --git a/src/Hyperbee.Json/Filters/Parser/Expressions/LiteralExpressionFactory.cs b/src/Hyperbee.Json/Filters/Parser/Expressions/LiteralExpressionFactory.cs index c4d9873e..ac037998 100644 --- a/src/Hyperbee.Json/Filters/Parser/Expressions/LiteralExpressionFactory.cs +++ b/src/Hyperbee.Json/Filters/Parser/Expressions/LiteralExpressionFactory.cs @@ -6,11 +6,11 @@ internal class LiteralExpressionFactory : IExpressionFactory { public static bool TryGetExpression( ref ParserState state, out Expression expression, FilterContext context ) { - expression = GetLiteralExpression( state.Item ); + expression = GetLiteralExpression( state.Item, context ); return expression != null; } - private static ConstantExpression GetLiteralExpression( ReadOnlySpan item ) + private static ConstantExpression GetLiteralExpression( ReadOnlySpan item, FilterContext context ) { // Check for known literals (true, false, null) first @@ -29,11 +29,12 @@ private static ConstantExpression GetLiteralExpression( ReadOnlySpan item return Expression.Constant( item[1..^1].ToString() ); // remove quotes // Check for numbers - // TODO: Currently assuming all numbers are floats since we don't know what's in the data or the other side of the operator yet. + // + // 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 ( float.TryParse( item, out float result ) ) - return Expression.Constant( result ); - - return null; + return float.TryParse( item, out float result ) + ? Expression.Constant( result ) + : null; } } diff --git a/src/Hyperbee.Json/Filters/Parser/FilterParser.cs b/src/Hyperbee.Json/Filters/Parser/FilterParser.cs index b7853f0c..246b2c5a 100644 --- a/src/Hyperbee.Json/Filters/Parser/FilterParser.cs +++ b/src/Hyperbee.Json/Filters/Parser/FilterParser.cs @@ -21,8 +21,6 @@ public abstract class FilterParser public const char EndLine = '\n'; public const char EndArg = ')'; public const char ArgSeparator = ','; - - protected static readonly MethodInfo ObjectEquals = typeof( object ).GetMethod( "Equals", [typeof( object ), typeof( object )] ); } public class FilterParser : FilterParser @@ -93,7 +91,10 @@ private static ExprItem GetExprItem( ref ParserState state, FilterContext if ( LiteralExpressionFactory.TryGetExpression( ref state, out expression, context ) ) return ExprItem( ref state, expression ); - throw new ArgumentException( $"Unsupported literal: {state.Buffer.ToString()}" ); + if ( JsonExpressionFactory.TryGetExpression( ref state, out expression, context ) ) + return ExprItem( ref state, expression ); + + throw new NotSupportedException( $"Unsupported literal: {state.Buffer.ToString()}" ); // Helper method to create an expression item static ExprItem ExprItem( ref ParserState state, Expression expression ) @@ -307,120 +308,79 @@ Operator.LessThan or private static void MergeItems( ExprItem left, ExprItem right, ITypeDescriptor descriptor ) { - // Ensure both expressions are value expressions - left.Expression = ExpressionConverter.ConvertToValue( descriptor.Accessor, left.Expression ); - right.Expression = ExpressionConverter.ConvertToValue( descriptor.Accessor, right.Expression ); - - left.Expression = left.Operator switch + switch ( left.Operator ) { - Operator.Equals when IsNumerical( left.Expression?.Type ) || IsNumerical( right.Expression.Type ) => - Expression.Equal( - ExpressionConverter.ConvertToNumber( left.Expression ), - ExpressionConverter.ConvertToNumber( right.Expression ) ), - - Operator.NotEquals when IsNumerical( left.Expression?.Type ) || IsNumerical( right.Expression.Type ) => - Expression.NotEqual( - ExpressionConverter.ConvertToNumber( left.Expression ), - ExpressionConverter.ConvertToNumber( right.Expression ) ), - - Operator.GreaterThan => - Expression.GreaterThan( - ExpressionConverter.ConvertToNumber( left.Expression ), - ExpressionConverter.ConvertToNumber( right.Expression ) ), - - Operator.GreaterThanOrEqual => - Expression.GreaterThanOrEqual( - ExpressionConverter.ConvertToNumber( left.Expression ), - ExpressionConverter.ConvertToNumber( right.Expression ) ), - - Operator.LessThan => - Expression.LessThan( - ExpressionConverter.ConvertToNumber( left.Expression ), - ExpressionConverter.ConvertToNumber( right.Expression ) ), - - Operator.LessThanOrEqual => - Expression.LessThanOrEqual( - ExpressionConverter.ConvertToNumber( left.Expression ), - ExpressionConverter.ConvertToNumber( right.Expression ) ), - - Operator.Equals => Equal( left.Expression, right.Expression ), - Operator.NotEquals => NotEqual( left.Expression, right.Expression ), - Operator.And => Expression.AndAlso( left.Expression!, right.Expression ), - Operator.Or => Expression.OrElse( left.Expression!, right.Expression ), - Operator.Not => Expression.Not( right.Expression ), - _ => left.Expression - }; - - // Wrap left expression in a try-catch block to handle exceptions - left.Expression = left.Expression == null - ? left.Expression - : Expression.TryCatchFinally( - left.Expression, - Expression.Empty(), // Ensure finally block is present - Expression.Catch( typeof( Exception ), Expression.Constant( false ) ) - ); + case Operator.Equals: + left.Expression = ComparerExpressionFactory.GetComparand( descriptor.Accessor, left.Expression ); + right.Expression = ComparerExpressionFactory.GetComparand( descriptor.Accessor, right.Expression ); - left.Operator = right.Operator; - return; - - // Helper method to determine if a type is numerical - static bool IsNumerical( Type type ) => type == typeof( float ) || type == typeof( int ); - - // Helper methods to create comparison expressions - static Expression Equal( Expression l, Expression r ) => Expression.Call( FilterParser.ObjectEquals, l, r ); - static Expression NotEqual( Expression l, Expression r ) => Expression.Not( Equal( l, r ) ); - } - - private static class ExpressionConverter - { - // Cached delegate for calling IValueAccessorGetAsValue( IEnumerable nodes ) - - private static readonly Func, IEnumerable, object> GetAsValueDelegate; - - static ExpressionConverter() - { - // Pre-compile the delegate to call the GetAsValue method - - var accessorParam = Expression.Parameter( typeof( IValueAccessor ), "accessor" ); - var expressionParam = Expression.Parameter( typeof( IEnumerable ), "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 ); - var methodInfo = typeof( IValueAccessor ).GetMethod( nameof( IValueAccessor.GetAsValue ) ); - var callExpression = Expression.Call( accessorParam, methodInfo!, expressionParam ); + 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 ); - GetAsValueDelegate = Expression.Lambda, IEnumerable, object>>( - callExpression, accessorParam, expressionParam ).Compile(); - } + 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 ); - public static Expression ConvertToValue( IValueAccessor accessor, Expression expression ) - { - if ( expression == null || expression.Type != typeof( IEnumerable ) ) - return expression; + 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 ); - // Create an expression representing the instance of the accessor - var accessorExpression = Expression.Constant( accessor ); + 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 ); - // Use the compiled delegate to create an expression to call the GetAsValue method - return Expression.Invoke( Expression.Constant( GetAsValueDelegate ), accessorExpression, expression ); + 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; } - // Helper method to convert numerical types to float - public static Expression ConvertToNumber( Expression expression ) - { - if ( expression.Type == typeof( float ) ) // quick out - return expression; - - if ( expression.Type == typeof( object ) || - expression.Type == typeof( int ) || - expression.Type == typeof( short ) || - expression.Type == typeof( long ) || - expression.Type == typeof( double ) || - expression.Type == typeof( decimal ) ) - { - return Expression.Convert( expression, typeof( float ) ); - } + // 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 ) ) + ); - return expression; - } + left.Operator = right.Operator; } private class ExprItem( Expression expression, Operator op ) diff --git a/src/Hyperbee.Json/Filters/Parser/FilterTruthyExpression.cs b/src/Hyperbee.Json/Filters/Parser/FilterTruthyExpression.cs index 3e543461..778159a0 100644 --- a/src/Hyperbee.Json/Filters/Parser/FilterTruthyExpression.cs +++ b/src/Hyperbee.Json/Filters/Parser/FilterTruthyExpression.cs @@ -1,4 +1,5 @@ using System.Collections; +using System.Linq.Expressions; using System.Reflection; namespace Hyperbee.Json.Filters.Parser @@ -7,10 +8,10 @@ public static class FilterTruthyExpression { private static readonly MethodInfo IsTruthyMethodInfo = typeof( FilterTruthyExpression ).GetMethod( nameof( IsTruthy ) ); - public static System.Linq.Expressions.Expression IsTruthyExpression( System.Linq.Expressions.Expression expression ) => + public static Expression IsTruthyExpression( Expression expression ) => expression.Type == typeof( bool ) ? expression - : System.Linq.Expressions.Expression.Call( IsTruthyMethodInfo, expression ); + : Expression.Call( IsTruthyMethodInfo, expression ); public static bool IsTruthy( object value ) { diff --git a/src/Hyperbee.Json/Internal/JsonElementAccessor.cs b/src/Hyperbee.Json/Internal/JsonElementAccessor.cs new file mode 100644 index 00000000..99167f89 --- /dev/null +++ b/src/Hyperbee.Json/Internal/JsonElementAccessor.cs @@ -0,0 +1,70 @@ +using System.Reflection; +using System.Reflection.Emit; +using System.Text.Json; + +namespace Hyperbee.Json.Internal; + +internal static class JsonElementAccessor +{ + // We need to identify an element's unique metadata location to establish instance identity. + // Deeply nested elements can have the same value but different locations in the document. + // Deep compare is not sufficient to establish instance identity in such cases from either a + // correctness or performance perspective. We can use the private _idx field of JsonElement + // to identify the element's unique location in the document. + // + // The justifications for this usage are: + // + // Performance Necessity: We need to access internal details to significantly improve + // performance and there is no viable public API that provides the required functionality. + // + // Lack of Alternatives: The desired functionality is not exposed by the public API and no + // alternative methods are available to achieve the same result. + // + // Low Risk: The usage is low risk because the internal field is only used to identify the + // uniqueness of the element in the document. The field is not modified and the document is + // not mutated. The field is read-only and the document is immutable. Furthermore, the field + // is only accessed through a delegate that is created once and reused for all instances of + // JsonElement. The delegate is created using a DynamicMethod and is not exposed to the + // public API. The delegate is used to access the field in a safe and controlled manner. + // + // The internal field is critical to Microsoft's internal implementation and is unlikely to + // change. + + internal static readonly Func GetIdx; + internal static readonly Func GetParent; + + static JsonElementAccessor() + { + // Create DynamicMethod to read the _idx field + const string idxName = "_idx"; + + var idxField = typeof( JsonElement ).GetField( idxName, BindingFlags.NonPublic | BindingFlags.Instance ); + + if ( idxField == null ) + throw new MissingFieldException( nameof( JsonElement ), idxName ); + + var getIdxDynamicMethod = new DynamicMethod( nameof( GetIdx ), typeof( int ), [typeof( JsonElement )], typeof( JsonElement ) ); + var ilIdx = getIdxDynamicMethod.GetILGenerator(); + ilIdx.Emit( OpCodes.Ldarg_0 ); + ilIdx.Emit( OpCodes.Ldfld, idxField ); + ilIdx.Emit( OpCodes.Ret ); + + GetIdx = (Func) getIdxDynamicMethod.CreateDelegate( typeof( Func ) ); + + // Create DynamicMethod to read the _parent field + const string parentName = "_parent"; + + var parentField = typeof( JsonElement ).GetField( parentName, BindingFlags.NonPublic | BindingFlags.Instance ); + + if ( parentField == null ) + throw new MissingFieldException( nameof( JsonElement ), parentName ); + + var getParentDynamicMethod = new DynamicMethod( nameof( GetParent ), typeof( JsonDocument ), [typeof( JsonElement )], typeof( JsonElement ) ); + var ilParent = getParentDynamicMethod.GetILGenerator(); + ilParent.Emit( OpCodes.Ldarg_0 ); + ilParent.Emit( OpCodes.Ldfld, parentField ); + ilParent.Emit( OpCodes.Ret ); + + GetParent = (Func) getParentDynamicMethod.CreateDelegate( typeof( Func ) ); + } +} diff --git a/src/Hyperbee.Json/Memory/SpanSplitOptions.cs b/src/Hyperbee.Json/Internal/SpanSplitOptions.cs similarity index 73% rename from src/Hyperbee.Json/Memory/SpanSplitOptions.cs rename to src/Hyperbee.Json/Internal/SpanSplitOptions.cs index fc9693df..f92259c0 100644 --- a/src/Hyperbee.Json/Memory/SpanSplitOptions.cs +++ b/src/Hyperbee.Json/Internal/SpanSplitOptions.cs @@ -1,4 +1,4 @@ -namespace Hyperbee.Json.Memory; +namespace Hyperbee.Json.Internal; [Flags] internal enum SpanSplitOptions diff --git a/src/Hyperbee.Json/Memory/SpanSplitter.cs b/src/Hyperbee.Json/Internal/SpanSplitter.cs similarity index 92% rename from src/Hyperbee.Json/Memory/SpanSplitter.cs rename to src/Hyperbee.Json/Internal/SpanSplitter.cs index 0b3a4a5f..81f44d68 100644 --- a/src/Hyperbee.Json/Memory/SpanSplitter.cs +++ b/src/Hyperbee.Json/Internal/SpanSplitter.cs @@ -1,12 +1,12 @@ -namespace Hyperbee.Json.Memory; +namespace Hyperbee.Json.Internal; // copied here from Hyperbee.Core to prevent additional assembly dependency /* - var splitter = new SpanSplitter( span, ',', SpanSplitOptions.RemoveEmptyEntries ); - while ( splitter.TryMoveNext(out var slice) ) - { - // ... - } + var splitter = new SpanSplitter( span, ',', SpanSplitOptions.RemoveEmptyEntries ); + while ( splitter.TryMoveNext(out var slice) ) + { + // ... + } */ internal ref struct SpanSplitter where T : IEquatable diff --git a/src/Hyperbee.Json/JsonElementDeepEqualsComparer.cs b/src/Hyperbee.Json/JsonElementDeepEqualityComparer.cs similarity index 95% rename from src/Hyperbee.Json/JsonElementDeepEqualsComparer.cs rename to src/Hyperbee.Json/JsonElementDeepEqualityComparer.cs index a313c24d..15fb5dd4 100644 --- a/src/Hyperbee.Json/JsonElementDeepEqualsComparer.cs +++ b/src/Hyperbee.Json/JsonElementDeepEqualityComparer.cs @@ -16,7 +16,7 @@ namespace Hyperbee.Json; // example 1: // -// var comparer = new JsonElementDeepEqualsComparer(); +// var comparer = new JsonElementDeepEqualityComparer(); // using var doc1 = JsonDocument.Parse( referenceJson ); // using var doc2 = JsonDocument.Parse( resultJson ); // @@ -30,13 +30,13 @@ namespace Hyperbee.Json; // var result = doc1.RootElement.DeepEquals( doc2.RootElement ); // -public class JsonElementDeepEqualsComparer : IEqualityComparer +public class JsonElementDeepEqualityComparer : IEqualityComparer { - public JsonElementDeepEqualsComparer() + public JsonElementDeepEqualityComparer() { } - public JsonElementDeepEqualsComparer( int maxHashDepth ) => MaxHashDepth = maxHashDepth; + public JsonElementDeepEqualityComparer( int maxHashDepth ) => MaxHashDepth = maxHashDepth; private int MaxHashDepth { get; } diff --git a/src/Hyperbee.Json/JsonElementInternal.cs b/src/Hyperbee.Json/JsonElementInternal.cs deleted file mode 100644 index 8f0f7d36..00000000 --- a/src/Hyperbee.Json/JsonElementInternal.cs +++ /dev/null @@ -1,48 +0,0 @@ -using System.Reflection; -using System.Reflection.Emit; -using System.Text.Json; - -namespace Hyperbee.Json; - -internal static class JsonElementInternal -{ - internal static readonly Func GetIdx; - internal static readonly Func GetParent; - - static JsonElementInternal() - { - // Create DynamicMethod for _idx field - - const string idxName = "_idx"; - - var idxField = typeof( JsonElement ).GetField( idxName, BindingFlags.NonPublic | BindingFlags.Instance ); - - if ( idxField == null ) - throw new MissingFieldException( nameof( JsonElement ), idxName ); - - var getIdxDynamicMethod = new DynamicMethod( nameof( GetIdx ), typeof( int ), [typeof( JsonElement )], typeof( JsonElement ) ); - var ilIdx = getIdxDynamicMethod.GetILGenerator(); - ilIdx.Emit( OpCodes.Ldarg_0 ); - ilIdx.Emit( OpCodes.Ldfld, idxField ); - ilIdx.Emit( OpCodes.Ret ); - - GetIdx = (Func) getIdxDynamicMethod.CreateDelegate( typeof( Func ) ); - - // Create DynamicMethod for _parent field - - const string parentName = "_parent"; - - var parentField = typeof( JsonElement ).GetField( parentName, BindingFlags.NonPublic | BindingFlags.Instance ); - - if ( parentField == null ) - throw new MissingFieldException( nameof( JsonElement ), parentName ); - - var getParentDynamicMethod = new DynamicMethod( nameof( GetParent ), typeof( JsonDocument ), [typeof( JsonElement )], typeof( JsonElement ) ); - var ilParent = getParentDynamicMethod.GetILGenerator(); - ilParent.Emit( OpCodes.Ldarg_0 ); - ilParent.Emit( OpCodes.Ldfld, parentField ); - ilParent.Emit( OpCodes.Ret ); - - GetParent = (Func) getParentDynamicMethod.CreateDelegate( typeof( Func ) ); - } -} diff --git a/src/Hyperbee.Json/JsonPath.cs b/src/Hyperbee.Json/JsonPath.cs index 76ca5502..66645e78 100644 --- a/src/Hyperbee.Json/JsonPath.cs +++ b/src/Hyperbee.Json/JsonPath.cs @@ -32,69 +32,72 @@ #endregion +using System.Diagnostics; using System.Globalization; using System.Runtime.CompilerServices; using Hyperbee.Json.Descriptors; -using Hyperbee.Json.Memory; + +// https://www.rfc-editor.org/rfc/rfc9535.html +// https://www.rfc-editor.org/rfc/rfc9535.html#appendix-A namespace Hyperbee.Json; -// https://www.rfc-editor.org/rfc/rfc9535.html +public delegate void NodeProcessorDelegate( in TNode parent, in TNode value, string key, in JsonPathSegment segment ); public static class JsonPath { - private record struct NodeArgs( in TNode Value, in JsonPathSegment Segment ); + [Flags] + internal enum NodeFlags + { + Default = 0, + AfterDescent = 1 + } private static readonly ITypeDescriptor Descriptor = JsonTypeDescriptorRegistry.GetDescriptor(); - public static IEnumerable Select( in TNode value, string query ) + public static IEnumerable Select( in TNode value, string query, NodeProcessorDelegate processor = null ) { - return EnumerateMatches( value, value, query ); + return EnumerateMatches( value, value, query, processor ); } - internal static IEnumerable SelectInternal( in TNode value, TNode root, string query ) + internal static IEnumerable SelectInternal( in TNode value, TNode root, string query, NodeProcessorDelegate processor = null ) { - // this overload is required for reentrant filter select evaluations. - // it is intended for use by nameof(FilterFunction) implementations. - - return EnumerateMatches( value, root, query ); + return EnumerateMatches( value, root, query, processor ); } - private static IEnumerable EnumerateMatches( in TNode value, in TNode root, string query ) + private static IEnumerable EnumerateMatches( in TNode value, in TNode root, string query, NodeProcessorDelegate processor = null ) { - ArgumentException.ThrowIfNullOrWhiteSpace( query ); - - // quick out + if ( string.IsNullOrWhiteSpace( query ) ) // invalid per the RFC ABNF + return []; // Consensus: return empty array for empty query - if ( query == "$" || query == "@" ) + if ( query == "$" || query == "@" ) // quick out for everything return [value]; - // tokenize + var segmentNext = JsonPathQueryParser.Parse( query ).Next; // The first segment is always the root; skip it - var segmentNext = JsonPathQueryParser.Parse( query ); - - if ( !segmentNext.IsFinal ) - { - var selector = segmentNext.Selectors[0].Value; // first selector in segment - - if ( selector == "$" || selector == "@" ) - segmentNext = segmentNext.Next; - } - - return EnumerateMatches( root, new NodeArgs( value, segmentNext ) ); + return EnumerateMatches( root, new NodeArgs( default, value, default, segmentNext, NodeFlags.Default ), processor ); } - private static IEnumerable EnumerateMatches( TNode root, NodeArgs args ) + private static IEnumerable EnumerateMatches( TNode root, NodeArgs args, NodeProcessorDelegate processor = null ) { - var stack = new Stack( 16 ); + var stack = new NodeArgsStack(); + var (accessor, filterEvaluator) = Descriptor; do { // deconstruct next args - var (value, segmentNext) = args; + var (parent, value, key, segmentNext, flags) = args; + // call node processor if it exists and the `key` is not null. + // the key is null when a descent has re-pushed the descent target. + // this should be safe to skip; we will see its values later. + + if ( key != null ) + processor?.Invoke( parent, value, key, segmentNext ); + + // yield matches if ( segmentNext.IsFinal ) { yield return value; @@ -111,16 +114,21 @@ private static IEnumerable EnumerateMatches( TNode root, NodeArgs args ) // make sure we have a complex value - if ( !accessor.IsObjectOrArray( value ) ) - throw new InvalidOperationException( "Object or Array expected." ); + var nodeKind = accessor.GetNodeKind( value ); + + if ( nodeKind == NodeKind.Value ) + continue; // try to access object or array using name or index - if ( segmentCurrent.Singular ) + 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 ) ) { - Push( stack, childValue, segmentNext ); + stack.Push( value, childValue, selector, segmentNext ); } continue; @@ -130,37 +138,42 @@ private static IEnumerable EnumerateMatches( TNode root, NodeArgs args ) if ( selectorKind == SelectorKind.Wildcard ) { - foreach ( var (_, childKey, childKind) in accessor.EnumerateChildren( value ) ) + foreach ( var (childValue, childKey, childKind) in accessor.EnumerateChildren( value ) ) { - Push( stack, value, segmentNext.Prepend( childKey, childKind ) ); // (Name | Index) + // 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; - - // we can reduce push/pop operations, and related allocations, if we check - // segmentNext.IsFinal and directly yield when true where possible. - // - // if ( segmentNext.IsFinal && !childValue.IsObjectOrArray() ) - // yield return childValue; - // else - // Push( stack, value, segmentNext.Prepend( childKey, childKind ) ); - // - // unfortunately, this optimization impacts result ordering. the rfc states - // result order should be as close to json document order as possible. for - // that reason, we chose not to implement this type of performance optimization. } // descendant if ( selectorKind == SelectorKind.Descendant ) { - var descendantSegment = segmentNext.Prepend( "..", SelectorKind.Descendant ); - foreach ( var (childValue, _, _) in accessor.EnumerateChildren( value, includeValues: false ) ) // child arrays or objects only + foreach ( var (childValue, childKey, _) in accessor.EnumerateChildren( value, includeValues: false ) ) // child arrays or objects only { - Push( stack, childValue, descendantSegment ); // Descendant + stack.Push( value, childValue, childKey, segmentCurrent ); // Descendant } - Push( stack, value, segmentNext ); // process the current value + // Union Processing After Descent: If a union operator follows a descent operator, + // either directly or after intermediary selectors, it should only process simple values. + + stack.Push( parent, value, null, segmentNext, NodeFlags.AfterDescent ); // process the current value continue; } @@ -179,8 +192,17 @@ private static IEnumerable EnumerateMatches( TNode root, NodeArgs args ) { var result = filterEvaluator.Evaluate( selector[1..], childValue, root ); // remove leading '?' - if ( Truthy( result ) ) - Push( stack, value, segmentNext.Prepend( childKey, childKind ) ); // (Name | Index) + if ( !Truthy( result ) ) + continue; + + // 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; @@ -188,13 +210,13 @@ private static IEnumerable EnumerateMatches( TNode root, NodeArgs args ) // array [name1,name2,...] or [#,#,...] or [start:end:step] - if ( accessor.IsArray( value, out var length ) ) + if ( nodeKind == NodeKind.Array ) { // [#,#,...] if ( selectorKind == SelectorKind.Index ) { - Push( stack, accessor.GetElementAt( value, int.Parse( selector ) ), segmentNext ); + stack.Push( value, accessor.GetElementAt( value, int.Parse( selector ) ), selector, segmentNext ); continue; } @@ -202,16 +224,26 @@ private static IEnumerable EnumerateMatches( TNode root, NodeArgs args ) if ( selectorKind == SelectorKind.Slice ) { - ProcessSlice( stack, value, selector, segmentNext, accessor ); + foreach ( var index in EnumerateSlice( value, selector, accessor ) ) + { + stack.Push( value, accessor.GetElementAt( value, index ), index.ToString(), segmentNext ); + } continue; } // [name1,name2,...] var indexSegment = segmentNext.Prepend( selector, SelectorKind.Name ); + var length = accessor.GetArrayLength( value ); + for ( var index = length - 1; index >= 0; index-- ) { - Push( stack, accessor.GetElementAt( value, index ), indexSegment ); + var childValue = accessor.GetElementAt( value, index ); + + if ( flags == NodeFlags.AfterDescent && accessor.GetNodeKind( childValue ) != NodeKind.Value ) + continue; + + stack.Push( value, childValue, index.ToString(), indexSegment ); } continue; @@ -219,58 +251,70 @@ private static IEnumerable EnumerateMatches( TNode root, NodeArgs args ) // object [name1,name2,...] - if ( accessor.IsObject( value ) ) + if ( nodeKind == NodeKind.Object ) { if ( selectorKind == SelectorKind.Slice || selectorKind == SelectorKind.Index ) continue; if ( accessor.TryGetChildValue( value, selector, out var childValue ) ) - Push( stack, childValue, segmentNext ); + { + stack.Push( value, childValue, selector, segmentNext ); + } } } } while ( stack.TryPop( out args ) ); } - [MethodImpl( MethodImplOptions.AggressiveInlining )] - private static void Push( Stack stack, in TNode value, in JsonPathSegment segment ) - { - stack.Push( new NodeArgs( value, segment ) ); - } - [MethodImpl( MethodImplOptions.AggressiveInlining )] private static bool Truthy( object value ) { return value is not null and not IConvertible || Convert.ToBoolean( value, CultureInfo.InvariantCulture ); } - private static void ProcessSlice( Stack stack, TNode value, string sliceExpr, JsonPathSegment segmentNext, IValueAccessor accessor ) + private static IEnumerable EnumerateSlice( TNode value, string sliceExpr, IValueAccessor accessor ) { - if ( !accessor.IsArray( value, out var length ) ) - return; + var length = accessor.GetArrayLength( value ); + + if ( length == 0 ) + yield break; - var (lower, upper, step) = SliceSyntaxHelper.ParseExpression( sliceExpr, length, reverse: true ); + var (lower, upper, step) = JsonPathSliceSyntaxHelper.ParseExpression( sliceExpr, length, reverse: true ); switch ( step ) { case > 0: { for ( var index = lower; index < upper; index += step ) - { - Push( stack, accessor.GetElementAt( value, index ), segmentNext ); - } - + yield return index; break; } case < 0: { for ( var index = upper; index > lower; index += step ) - { - Push( stack, accessor.GetElementAt( value, index ), segmentNext ); - } - + yield return index; break; } } } + + [DebuggerDisplay( "Parent = {Parent}, Value = {Value}, First = ({Segment?.Selectors?[0]}), IsSingular = {Segment?.IsSingular}, Count = {Segment?.Selectors?.Length}" )] + private record struct NodeArgs( TNode Parent, TNode Value, string Key, JsonPathSegment Segment, NodeFlags Flags ); + + private sealed class NodeArgsStack( int capacity = 16 ) + { + private readonly Stack _stack = new( capacity ); + + [MethodImpl( MethodImplOptions.AggressiveInlining )] + public void Push( in TNode parent, in TNode value, string key, in JsonPathSegment segment, NodeFlags flags = NodeFlags.Default ) + { + _stack.Push( new NodeArgs( parent, value, key, segment, flags ) ); + } + + [MethodImpl( MethodImplOptions.AggressiveInlining )] + public bool TryPop( out NodeArgs args ) + { + return _stack.TryPop( out args ); + } + } } diff --git a/src/Hyperbee.Json/JsonPathBuilder.cs b/src/Hyperbee.Json/JsonPathBuilder.cs new file mode 100644 index 00000000..0f76e09b --- /dev/null +++ b/src/Hyperbee.Json/JsonPathBuilder.cs @@ -0,0 +1,179 @@ +using System.Runtime.CompilerServices; +using System.Text; +using System.Text.Json; +using Hyperbee.Json.Internal; + +namespace Hyperbee.Json; + +public class JsonPathBuilder +{ + private readonly JsonElement _rootElement; + private readonly JsonElementPositionComparer _comparer = new(); + private readonly Dictionary _parentMap = []; + + public JsonPathBuilder( JsonDocument rootDocument ) + : this( rootDocument.RootElement ) + { + } + + public JsonPathBuilder( JsonElement rootElement ) + { + _rootElement = rootElement; + + // avoid allocating full paths for every node by building + // a dictionary of (parentId, segment) pairs. + + _parentMap[GetUniqueId( _rootElement )] = (-1, "$"); // seed parent map with root + } + + public string GetPath( in JsonElement targetElement ) + { + // quick out + + var targetId = GetUniqueId( targetElement ); + + if ( _parentMap.ContainsKey( targetId ) ) + return BuildPath( targetId, _parentMap ); + + // take a walk + + var stack = new Stack( [_rootElement] ); + + while ( stack.Count > 0 ) + { + var currentElement = stack.Pop(); + var elementId = GetUniqueId( currentElement ); + + if ( _comparer.Equals( currentElement, targetElement ) ) + return BuildPath( elementId, _parentMap ); + + switch ( currentElement.ValueKind ) + { + case JsonValueKind.Object: + foreach ( var property in currentElement.EnumerateObject() ) + { + var itemId = GetUniqueId( property.Value ); + + if ( _parentMap.ContainsKey( itemId ) ) + continue; + + _parentMap[itemId] = (elementId, $".{property.Name}"); + stack.Push( property.Value ); + } + break; + + case JsonValueKind.Array: + var arrayIdx = 0; + foreach ( var item in currentElement.EnumerateArray() ) + { + var itemId = GetUniqueId( item ); + + if ( _parentMap.ContainsKey( itemId ) ) + continue; + + _parentMap[itemId] = (elementId, $"[{arrayIdx++}]"); + stack.Push( item ); + } + break; + } + } + + return null; // target not found + } + + // This method is called by `SelectPath` to pre-seed the parent map. + // This optimization allows us to leverage the select path walk, so + // we won't have to walk again when `BuildPath` is called. + internal void InsertItem( in JsonElement parentElement, in JsonElement itemElement, string itemKey ) + { + var itemId = GetUniqueId( itemElement ); + + if ( _parentMap.ContainsKey( itemId ) ) + return; + + var parentId = parentElement.ValueKind == JsonValueKind.Undefined + ? GetUniqueId( _rootElement ) + : GetUniqueId( parentElement ); + + itemKey = parentElement.ValueKind == JsonValueKind.Array + ? $"[{itemKey}]" + : $".{itemKey}"; + + _parentMap[itemId] = (parentId, itemKey); + } + + [MethodImpl( MethodImplOptions.AggressiveInlining )] + private static int GetUniqueId( in JsonElement element ) + { + return JsonElementAccessor.GetIdx( element ); + } + + private static string BuildPath( in int currentId, Dictionary parentMap ) + { + // use recursion to reduce allocations by avoiding a stack + + var builder = new StringBuilder( 64 ); + + RecursiveBuildPath( parentMap, currentId, builder ); + + return builder.ToString(); + + // Helper to build path from its parts + + static void RecursiveBuildPath( Dictionary parentMap, int currentId, StringBuilder builder ) + { + if ( currentId == -1 ) + return; + + var (parentId, segment) = parentMap[currentId]; + RecursiveBuildPath( parentMap, parentId, builder ); + builder.Append( segment ); + } + } + + // We want a fast comparer that will tell us if two JsonElements point to the same + // location in the JsonDocument. JsonElement is a struct, and a value comparison + // for equality won't give us reliable results and a deep compare would be + // operationally expensive. + + private class JsonElementPositionComparer : IEqualityComparer + { + public bool Equals( JsonElement x, JsonElement y ) + { + // check for quick out + + if ( x.ValueKind != y.ValueKind ) + return false; + + // The internal JsonElement constructor takes parent and idx arguments that are saved as + // immutable fields. + // + // idx: is an index used to get the unique position of the JsonElement in the backing metadata. + // parent: is the owning JsonDocument (could be null in an enumeration). + // + // These arguments are stored in private fields and are not exposed. While not ideal, we will + // directly access these fields through dynamic methods to use for our comparison. If microsoft + // provides Parent and Location in the future we will remove this. + + // check parent documents + + var xParent = JsonElementAccessor.GetParent( x ); + var yParent = JsonElementAccessor.GetParent( y ); + + if ( !ReferenceEquals( xParent, yParent ) ) + return false; + + // check idx values + + return JsonElementAccessor.GetIdx( x ) == JsonElementAccessor.GetIdx( y ); + } + + public int GetHashCode( JsonElement obj ) + { + var parent = JsonElementAccessor.GetParent( obj ); + var idx = JsonElementAccessor.GetIdx( obj ); + + return HashCode.Combine( parent, idx ); + } + } +} diff --git a/src/Hyperbee.Json/JsonPathQueryParser.cs b/src/Hyperbee.Json/JsonPathQueryParser.cs index 0aed554d..255cceac 100644 --- a/src/Hyperbee.Json/JsonPathQueryParser.cs +++ b/src/Hyperbee.Json/JsonPathQueryParser.cs @@ -27,7 +27,7 @@ public enum SelectorKind Descendant = 0x200 | Group } -public static class JsonPathQueryParser +internal static class JsonPathQueryParser { private static readonly ConcurrentDictionary JsonPathTokens = new(); @@ -83,6 +83,8 @@ private static JsonPathSegment TokenFactory( ReadOnlySpan query ) var tokens = new List(); + query = query.TrimEnd(); // remove trailing whitespace to simplify parsing + var i = 0; var n = query.Length; @@ -114,6 +116,10 @@ private static JsonPathSegment TokenFactory( ReadOnlySpan query ) 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; break; default: @@ -354,10 +360,10 @@ private static JsonPathSegment TokenFactory( ReadOnlySpan query ) // return tokenized query as a segment list - return TokensAsSegment( tokens ); + return TokensToSegment( tokens ); } - private static JsonPathSegment TokensAsSegment( IList tokens ) + private static JsonPathSegment TokensToSegment( IList tokens ) { if ( tokens == null || tokens.Count == 0 ) return JsonPathSegment.Final; diff --git a/src/Hyperbee.Json/JsonPathResolver.cs b/src/Hyperbee.Json/JsonPathResolver.cs deleted file mode 100644 index 809e0af3..00000000 --- a/src/Hyperbee.Json/JsonPathResolver.cs +++ /dev/null @@ -1,146 +0,0 @@ -using System.Text.Json; - -namespace Hyperbee.Json; - -public class JsonPathResolver -{ - private readonly JsonElement _rootElement; - private readonly JsonElementPositionComparer _comparer = new(); - private readonly Dictionary _parentMap = []; - - public JsonPathResolver( JsonDocument rootDocument ) - : this( rootDocument.RootElement ) - { - } - - public JsonPathResolver( JsonElement rootElement ) - { - _rootElement = rootElement; - - // avoid allocating full paths for every node by building - // a dictionary of (parentId, segment) pairs. - - _parentMap[GetUniqueId( _rootElement )] = (-1, "$"); // seed parent map with root - } - - public string GetPath( in JsonElement targetElement ) - { - // quick out - - var targetId = GetUniqueId( targetElement ); - - if ( _parentMap.ContainsKey( targetId ) ) - return BuildPath( targetId, _parentMap ); - - // take a walk - - var stack = new Stack( [_rootElement] ); - - while ( stack.Count > 0 ) - { - var currentElement = stack.Pop(); - var elementId = GetUniqueId( currentElement ); - - if ( _comparer.Equals( currentElement, targetElement ) ) - return BuildPath( elementId, _parentMap ); - - switch ( currentElement.ValueKind ) - { - case JsonValueKind.Object: - foreach ( var property in currentElement.EnumerateObject() ) - { - var childElementId = GetUniqueId( property.Value ); - - if ( !_parentMap.ContainsKey( childElementId ) ) - _parentMap[childElementId] = (elementId, $".{property.Name}"); - - stack.Push( property.Value ); - } - break; - - case JsonValueKind.Array: - var arrayIdx = 0; - foreach ( var element in currentElement.EnumerateArray() ) - { - var childElementId = GetUniqueId( element ); - - if ( !_parentMap.ContainsKey( childElementId ) ) - _parentMap[childElementId] = (elementId, $"[{arrayIdx}]"); - - stack.Push( element ); - arrayIdx++; - } - break; - } - } - - return null; // target not found - } - - private static int GetUniqueId( in JsonElement element ) - { - return JsonElementInternal.GetIdx( element ); - } - - private static string BuildPath( in int elementId, Dictionary parentMap ) - { - var pathSegments = new Stack(); - var currentId = elementId; - - while ( currentId != -1 ) - { - var (parentId, segment) = parentMap[currentId]; - pathSegments.Push( segment ); - currentId = parentId; - } - - return string.Join( string.Empty, pathSegments ); - } - - // We want a fast comparer that will tell us if two JsonElements point to the same exact - // backing data in the parent JsonDocument. JsonElement is a struct, and a value comparison - // for equality won't give us reliable results and would be expensive. - // - private class JsonElementPositionComparer : IEqualityComparer - { - public bool Equals( JsonElement x, JsonElement y ) - { - // check for quick out - - if ( x.ValueKind != y.ValueKind ) - return false; - - // The internal JsonElement constructor takes parent and idx arguments that are saved as fields. - // - // idx: is an index used to get the position of the JsonElement in the backing data. - // parent: is the owning JsonDocument (could be null in an enumeration). - // - // These arguments are stored in private fields and are not exposed. While not ideal, we will - // directly access these fields through dynamic methods to use for our comparison. If microsoft - // provides Parent and Location in the future we will remove this. - - // check parent documents - - // The JsonElement ctor notes that parent may be null in some enumeration conditions. - // This check may not be reliable. If so, should be ok to remove the parent check. - - var xParent = JsonElementInternal.GetParent( x ); - var yParent = JsonElementInternal.GetParent( y ); - - if ( !ReferenceEquals( xParent, yParent ) ) - return false; - - // check idx values - - return JsonElementInternal.GetIdx( x ) == JsonElementInternal.GetIdx( y ); - } - - public int GetHashCode( JsonElement obj ) - { - var parent = JsonElementInternal.GetParent( obj ); - var idx = JsonElementInternal.GetIdx( obj ); - - return HashCode.Combine( parent, idx ); - } - } -} diff --git a/src/Hyperbee.Json/JsonPathSegment.cs b/src/Hyperbee.Json/JsonPathSegment.cs index 47d66b29..5d95c1c4 100644 --- a/src/Hyperbee.Json/JsonPathSegment.cs +++ b/src/Hyperbee.Json/JsonPathSegment.cs @@ -3,7 +3,7 @@ namespace Hyperbee.Json; [DebuggerDisplay( "{Value}, SelectorKind = {SelectorKind}" )] -internal record SelectorDescriptor +public record SelectorDescriptor { public SelectorKind SelectorKind { get; init; } public string Value { get; init; } @@ -16,14 +16,14 @@ public void Deconstruct( out string value, out SelectorKind selectorKind ) } [DebuggerTypeProxy( typeof( SegmentDebugView ) )] -[DebuggerDisplay( "First = ({Selectors?[0]}), Singular = {Singular}, Count = {Selectors?.Length}" )] -internal class JsonPathSegment +[DebuggerDisplay( "First = ({Selectors?[0]}), IsSingular = {IsSingular}, Count = {Selectors?.Length}" )] +public class JsonPathSegment { internal static readonly JsonPathSegment Final = new(); // special end node public bool IsFinal => Next == null; - public bool Singular { get; } // singular is true when the selector resolves to one and only one element + public bool IsSingular { get; } // singular is true when the selector resolves to one and only one element public JsonPathSegment Next { get; set; } public SelectorDescriptor[] Selectors { get; init; } @@ -37,16 +37,19 @@ public JsonPathSegment( JsonPathSegment next, string selector, SelectorKind kind [ new SelectorDescriptor { SelectorKind = kind, Value = selector } ]; - Singular = IsSingular(); + IsSingular = InitIsSingular(); } public JsonPathSegment( SelectorDescriptor[] selectors ) { Selectors = selectors; - Singular = IsSingular(); + IsSingular = InitIsSingular(); } - public JsonPathSegment Prepend( string selector, SelectorKind kind ) => new( this, selector, kind ); + public JsonPathSegment Prepend( string selector, SelectorKind kind ) + { + return new JsonPathSegment( this, selector, kind ); + } public IEnumerable AsEnumerable() { @@ -60,8 +63,10 @@ public IEnumerable AsEnumerable() } } - private bool IsSingular() + private bool InitIsSingular() { + // singular is one selector that is not a group + if ( Selectors.Length != 1 ) return false; @@ -70,7 +75,7 @@ private bool IsSingular() public void Deconstruct( out bool singular, out SelectorDescriptor[] selectors ) { - singular = Singular; + singular = IsSingular; selectors = Selectors; } diff --git a/src/Hyperbee.Json/Memory/SliceSyntaxHelper.cs b/src/Hyperbee.Json/JsonPathSliceSyntaxHelper.cs similarity index 97% rename from src/Hyperbee.Json/Memory/SliceSyntaxHelper.cs rename to src/Hyperbee.Json/JsonPathSliceSyntaxHelper.cs index da611bfc..ecec7863 100644 --- a/src/Hyperbee.Json/Memory/SliceSyntaxHelper.cs +++ b/src/Hyperbee.Json/JsonPathSliceSyntaxHelper.cs @@ -1,8 +1,9 @@ using System.Globalization; +using Hyperbee.Json.Internal; -namespace Hyperbee.Json.Memory; +namespace Hyperbee.Json; -internal static class SliceSyntaxHelper +internal static class JsonPathSliceSyntaxHelper { public static (int Lower, int Upper, int Step) ParseExpression( ReadOnlySpan sliceExpr, int length, bool reverse = false ) { diff --git a/test/Hyperbee.Json.Benchmark/FilterExpressionParserEvaluator.cs b/test/Hyperbee.Json.Benchmark/FilterExpressionParserEvaluator.cs index fc81c635..1d7b301d 100644 --- a/test/Hyperbee.Json.Benchmark/FilterExpressionParserEvaluator.cs +++ b/test/Hyperbee.Json.Benchmark/FilterExpressionParserEvaluator.cs @@ -1,8 +1,6 @@ -using System.Linq.Expressions; -using System.Text.Json; +using System.Text.Json; using System.Text.Json.Nodes; using BenchmarkDotNet.Attributes; -using Hyperbee.Json.Descriptors; using Hyperbee.Json.Descriptors.Element; using Hyperbee.Json.Descriptors.Node; using Hyperbee.Json.Filters.Parser; diff --git a/test/Hyperbee.Json.Benchmark/Hyperbee.Json.Benchmark.csproj b/test/Hyperbee.Json.Benchmark/Hyperbee.Json.Benchmark.csproj index 3f5023ff..9f4b5ad6 100644 --- a/test/Hyperbee.Json.Benchmark/Hyperbee.Json.Benchmark.csproj +++ b/test/Hyperbee.Json.Benchmark/Hyperbee.Json.Benchmark.csproj @@ -16,7 +16,7 @@ - + diff --git a/test/Hyperbee.Json.Tests/Resolver/JsonPathBuilderTests.cs b/test/Hyperbee.Json.Tests/Builder/JsonPathBuilderTests.cs similarity index 67% rename from test/Hyperbee.Json.Tests/Resolver/JsonPathBuilderTests.cs rename to test/Hyperbee.Json.Tests/Builder/JsonPathBuilderTests.cs index 48c107ae..39dd921a 100644 --- a/test/Hyperbee.Json.Tests/Resolver/JsonPathBuilderTests.cs +++ b/test/Hyperbee.Json.Tests/Builder/JsonPathBuilderTests.cs @@ -3,7 +3,7 @@ using Hyperbee.Json.Tests.TestSupport; using Microsoft.VisualStudio.TestTools.UnitTesting; -namespace Hyperbee.Json.Tests.Resolver; +namespace Hyperbee.Json.Tests.Builder; [TestClass] public class JsonPathBuilderTests : JsonTestBase @@ -13,18 +13,14 @@ public class JsonPathBuilderTests : JsonTestBase [DataRow( "$['store']['book'][1]['author']", "$.store.book[1].author" )] [DataRow( "$['store']['book'][2]['author']", "$.store.book[2].author" )] [DataRow( "$['store']['book'][3]['author']", "$.store.book[3].author" )] - public void Should_GetPath( string key, string expected ) + public void Should_GetPath( string pointer, string expected ) { var source = GetDocument(); - var target = source.RootElement.GetPropertyFromPath( key ); + var target = source.RootElement.FromJsonPathPointer( pointer ); - var builder = new JsonPathResolver( source ); + var builder = new JsonPathBuilder( source ); var result = builder.GetPath( target ); Assert.AreEqual( result, expected ); - - var resultCached = builder.GetPath( target ); - - Assert.AreEqual( resultCached, expected ); } } diff --git a/test/Hyperbee.Json.Tests/Dynamic/JsonDynamicTests.cs b/test/Hyperbee.Json.Tests/Dynamic/JsonDynamicTests.cs index 908f94cf..f740a307 100644 --- a/test/Hyperbee.Json.Tests/Dynamic/JsonDynamicTests.cs +++ b/test/Hyperbee.Json.Tests/Dynamic/JsonDynamicTests.cs @@ -9,6 +9,8 @@ namespace Hyperbee.Json.Tests.Dynamic; [TestClass] public class JsonDynamicTests : JsonTestBase { + static readonly JsonSerializerOptions SerializerOptions = new() { Converters = { new DynamicJsonConverter() } }; + private enum Thing { ThisThing, @@ -16,7 +18,7 @@ private enum Thing } [TestMethod] - public void Dynamic_json_element_should_return_correct_results() + public void DynamicJsonElement_ShouldReturnCorrectResults() { var source = GetDocument(); var element = source.ToDynamic(); @@ -30,18 +32,13 @@ public void Dynamic_json_element_should_return_correct_results() } [TestMethod] - public void Dynamic_json_converter_should_return_correct_results() + public void DynamicJsonConverter_ShouldReturnCorrectResults() { - var serializerOptions = new JsonSerializerOptions - { - Converters = { new DynamicJsonConverter() } - }; - - var jobject = JsonSerializer.Deserialize( ReadJsonString(), serializerOptions ); + var jobject = JsonSerializer.Deserialize( ReadJsonString(), SerializerOptions ); jobject!.store.thing = Thing.ThatThing; - var output = JsonSerializer.Serialize( jobject, serializerOptions ) as string; + var output = JsonSerializer.Serialize( jobject, SerializerOptions ) as string; Assert.IsTrue( jobject.store.bicycle.color == "red" ); Assert.IsTrue( jobject.store.thing == Thing.ThatThing ); diff --git a/test/Hyperbee.Json.Tests/Extensions/JsonExtensionTests.cs b/test/Hyperbee.Json.Tests/Extensions/JsonExtensionTests.cs index 2c678964..42f1b368 100644 --- a/test/Hyperbee.Json.Tests/Extensions/JsonExtensionTests.cs +++ b/test/Hyperbee.Json.Tests/Extensions/JsonExtensionTests.cs @@ -15,7 +15,7 @@ public struct TestItem } [TestMethod] - public void Should_serialize_json_element_to_object() + public void Should_SerializeJsonElement_ToObject() { // arrange var source = new TestItem @@ -35,7 +35,7 @@ public void Should_serialize_json_element_to_object() } [TestMethod] - public void Should_return_property_value_for_property_path() + public void Should_ReturnPropertyValue_ForJsonPathPointer() { // arrange const string json = """ @@ -61,7 +61,7 @@ public void Should_return_property_value_for_property_path() var document = JsonDocument.Parse( json ); // act - var result = document.RootElement.GetPropertyFromPath( "assets[0].asset.['code']" ).GetString(); + var result = document.RootElement.FromJsonPathPointer( "$.assets[0].asset.['code']" ).GetString(); // asset Assert.AreEqual( "#load", result ); diff --git a/test/Hyperbee.Json.Tests/Hyperbee.Json.Tests.csproj b/test/Hyperbee.Json.Tests/Hyperbee.Json.Tests.csproj index 4e9f7b72..660bd272 100644 --- a/test/Hyperbee.Json.Tests/Hyperbee.Json.Tests.csproj +++ b/test/Hyperbee.Json.Tests/Hyperbee.Json.Tests.csproj @@ -5,7 +5,7 @@ Hyperbee.Json.Tests - + diff --git a/test/Hyperbee.Json.Tests/Parsers/FilterExtensionFunctionTests.cs b/test/Hyperbee.Json.Tests/Parsers/FilterExtensionFunctionTests.cs index 1ce59077..0a747303 100644 --- a/test/Hyperbee.Json.Tests/Parsers/FilterExtensionFunctionTests.cs +++ b/test/Hyperbee.Json.Tests/Parsers/FilterExtensionFunctionTests.cs @@ -19,7 +19,9 @@ public void Should_CallCustomFunction() // arrange var source = GetDocument(); - JsonTypeDescriptorRegistry.GetDescriptor().Functions + JsonTypeDescriptorRegistry + .GetDescriptor() + .Functions .Register( PathNodeFunction.Name, () => new PathNodeFunction() ); // act diff --git a/test/Hyperbee.Json.Tests/Parsers/FilterParserTests.cs b/test/Hyperbee.Json.Tests/Parsers/FilterParserTests.cs index ed697baa..8f2ded55 100644 --- a/test/Hyperbee.Json.Tests/Parsers/FilterParserTests.cs +++ b/test/Hyperbee.Json.Tests/Parsers/FilterParserTests.cs @@ -51,7 +51,15 @@ public void Should_MatchExpectedResult_WhenUsingConstants( string filter, bool e [DataTestMethod] [DataRow( "@.store.bicycle.price < 10", false, typeof( JsonElement ) )] + [DataRow( "@.store.bicycle.price <= 10", false, typeof( JsonElement ) )] + [DataRow( "@.store.bicycle.price < 20", true, typeof( JsonElement ) )] + [DataRow( "@.store.bicycle.price <= 20", true, typeof( JsonElement ) )] [DataRow( "@.store.bicycle.price > 15", true, typeof( JsonElement ) )] + [DataRow( "@.store.bicycle.price >= 15", true, typeof( JsonElement ) )] + [DataRow( "@.store.bicycle.price > 20", false, typeof( JsonElement ) )] + [DataRow( "@.store.bicycle.price >= 20", false, typeof( JsonElement ) )] + [DataRow( "@.store.bicycle.price == 19.95", true, typeof( JsonElement ) )] + [DataRow( "@.store.bicycle.price != 19.95", false, typeof( JsonElement ) )] [DataRow( "@.store.book[0].category == \"reference\"", true, typeof( JsonElement ) )] [DataRow( "@.store.book[0].category == 'reference'", true, typeof( JsonElement ) )] [DataRow( "@.store.book[0].category == @.store.book[1].category", false, typeof( JsonElement ) )] diff --git a/test/Hyperbee.Json.Tests/Parsers/JsonPathQueryParserTests.cs b/test/Hyperbee.Json.Tests/Parsers/JsonPathQueryParserTests.cs index 9ce26565..a89af0a5 100644 --- a/test/Hyperbee.Json.Tests/Parsers/JsonPathQueryParserTests.cs +++ b/test/Hyperbee.Json.Tests/Parsers/JsonPathQueryParserTests.cs @@ -30,7 +30,7 @@ public class JsonPathQueryParserTests [DataRow( "$..*", "{$|s};{..|g};{*|g}" )] [DataRow( """$.store.book[?(@path !== "$['store']['book'][0]")]""", """{$|s};{store|s};{book|s};{?(@path !== "$['store']['book'][0]")|g}""" )] [DataRow( """$..book[?(@.price == 8.99 && @.category == "fiction")]""", """{$|s};{..|g};{book|s};{?(@.price == 8.99 && @.category == "fiction")|g}""" )] - public void Should_tokenize_json_path( string jsonPath, string expected ) + public void Should_TokenizeJsonPath( string jsonPath, string expected ) { // act var pathSegment = JsonPathQueryParser.Parse( jsonPath ); diff --git a/test/Hyperbee.Json.Tests/Query/JsonComparerComparandTests.cs b/test/Hyperbee.Json.Tests/Query/JsonComparerComparandTests.cs new file mode 100644 index 00000000..b9eccdd6 --- /dev/null +++ b/test/Hyperbee.Json.Tests/Query/JsonComparerComparandTests.cs @@ -0,0 +1,131 @@ +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 ddd8c43c..b4bc9b87 100644 --- a/test/Hyperbee.Json.Tests/Query/JsonPathArrayTests.cs +++ b/test/Hyperbee.Json.Tests/Query/JsonPathArrayTests.cs @@ -15,7 +15,7 @@ public class JsonPathArrayTests : JsonTestBase [DataRow( "$[1:3]", typeof( JsonNode ) )] public void ArraySlice( string query, Type sourceType ) { - //consensus: ["second", "third"] + // consensus: ["second", "third"] const string json = """ [ @@ -26,13 +26,13 @@ public void ArraySlice( string query, Type sourceType ) "fifth" ] """; - var source = GetDocumentProxyFromSource( sourceType, json ); + var source = GetDocumentFromSource( sourceType, json ); - var matches = source.Select( query ); + var matches = source.Select( query ).ToList(); var expected = new[] { - source.GetPropertyFromPath("$[1]"), - source.GetPropertyFromPath("$[2]") + source.FromJsonPathPointer("$[1]"), + source.FromJsonPathPointer("$[2]") }; Assert.IsTrue( expected.SequenceEqual( matches ) ); @@ -43,7 +43,7 @@ public void ArraySlice( string query, Type sourceType ) [DataRow( "$[0:5]", typeof( JsonNode ) )] public void ArraySliceOnExactMatch( string query, Type sourceType ) { - //consensus: ["first", "second", "third", "forth", "fifth"] + // consensus: ["first", "second", "third", "forth", "fifth"] const string json = """ [ @@ -54,16 +54,16 @@ public void ArraySliceOnExactMatch( string query, Type sourceType ) "fifth" ] """; - var source = GetDocumentProxyFromSource( sourceType, json ); + var source = GetDocumentFromSource( sourceType, json ); var matches = source.Select( query ); var expected = new[] { - source.GetPropertyFromPath("$[0]"), - source.GetPropertyFromPath("$[1]"), - source.GetPropertyFromPath("$[2]"), - source.GetPropertyFromPath("$[3]"), - source.GetPropertyFromPath("$[4]") + source.FromJsonPathPointer("$[0]"), + source.FromJsonPathPointer("$[1]"), + source.FromJsonPathPointer("$[2]"), + source.FromJsonPathPointer("$[3]"), + source.FromJsonPathPointer("$[4]") }; Assert.IsTrue( expected.SequenceEqual( matches ) ); @@ -74,7 +74,7 @@ public void ArraySliceOnExactMatch( string query, Type sourceType ) [DataRow( "$[7:10]", typeof( JsonNode ) )] public void ArraySliceOnNonOverlappingArray( string query, Type sourceType ) { - //consensus: [] + // consensus: [] const string json = """ [ @@ -85,10 +85,10 @@ public void ArraySliceOnNonOverlappingArray( string query, Type sourceType ) "fifth" ] """; - var source = GetDocumentProxyFromSource( sourceType, json ); + var source = GetDocumentFromSource( sourceType, json ); var matches = source.Select( query ); - var expected = source.ArrayEmpty; + var expected = Enumerable.Empty(); Assert.IsTrue( expected.SequenceEqual( matches ) ); } @@ -98,7 +98,7 @@ public void ArraySliceOnNonOverlappingArray( string query, Type sourceType ) [DataRow( "$[1:3]", typeof( JsonNode ) )] public void ArraySliceOnObject( string query, Type sourceType ) { - //consensus: [] + // consensus: [] const string json = """ { @@ -110,10 +110,10 @@ public void ArraySliceOnObject( string query, Type sourceType ) "1:3": "nice" } """; - var source = GetDocumentProxyFromSource( sourceType, json ); + var source = GetDocumentFromSource( sourceType, json ); var matches = source.Select( query ); - var expected = source.ArrayEmpty; + var expected = Enumerable.Empty(); Assert.IsTrue( expected.SequenceEqual( matches ) ); } @@ -123,7 +123,7 @@ public void ArraySliceOnObject( string query, Type sourceType ) [DataRow( "$[1:10]", typeof( JsonNode ) )] public void ArraySliceOnPartiallyOverlappingArray( string query, Type sourceType ) { - //consensus: ["second", "third"] + // consensus: ["second", "third"] const string json = """ [ @@ -132,13 +132,13 @@ public void ArraySliceOnPartiallyOverlappingArray( string query, Type sourceType "third" ] """; - var source = GetDocumentProxyFromSource( sourceType, json ); + var source = GetDocumentFromSource( sourceType, json ); var matches = source.Select( query ); var expected = new[] { - source.GetPropertyFromPath("$[1]"), - source.GetPropertyFromPath("$[2]") + source.FromJsonPathPointer("$[1]"), + source.FromJsonPathPointer("$[2]") }; Assert.IsTrue( expected.SequenceEqual( matches ) ); @@ -149,7 +149,7 @@ public void ArraySliceOnPartiallyOverlappingArray( string query, Type sourceType [DataRow( "$[2:113667776004]", typeof( JsonNode ) )] public void ArraySliceWithLargeNumberForEnd( string query, Type sourceType ) { - //consensus: ["third", "forth", "fifth"] + // consensus: ["third", "forth", "fifth"] const string json = """ [ @@ -160,14 +160,14 @@ public void ArraySliceWithLargeNumberForEnd( string query, Type sourceType ) "fifth" ] """; - var source = GetDocumentProxyFromSource( sourceType, json ); + var source = GetDocumentFromSource( sourceType, json ); var matches = source.Select( query ); var expected = new[] { - source.GetPropertyFromPath("$[2]"), - source.GetPropertyFromPath("$[3]"), - source.GetPropertyFromPath("$[4]") + source.FromJsonPathPointer("$[2]"), + source.FromJsonPathPointer("$[3]"), + source.FromJsonPathPointer("$[4]") }; Assert.IsTrue( expected.SequenceEqual( matches ) ); @@ -178,8 +178,8 @@ public void ArraySliceWithLargeNumberForEnd( string query, Type sourceType ) [DataRow( "$[2:-113667776004:-1]", typeof( JsonNode ) )] public void ArraySliceWithLargeNumberForEndAndNegativeStep( string query, Type sourceType ) { - //consensus: //none - //implementation: ["third","second","first"] //rfc + // rfc: ["third","second","first"] + // consensus: none const string json = """ [ @@ -190,14 +190,14 @@ public void ArraySliceWithLargeNumberForEndAndNegativeStep( string query, Type s "fifth" ] """; - var source = GetDocumentProxyFromSource( sourceType, json ); + var source = GetDocumentFromSource( sourceType, json ); var matches = source.Select( query ); var expected = new[] { - source.GetPropertyFromPath("$[2]"), - source.GetPropertyFromPath("$[1]"), - source.GetPropertyFromPath("$[0]") + source.FromJsonPathPointer("$[2]"), + source.FromJsonPathPointer("$[1]"), + source.FromJsonPathPointer("$[0]") }; Assert.IsTrue( expected.SequenceEqual( matches ) ); @@ -208,7 +208,7 @@ public void ArraySliceWithLargeNumberForEndAndNegativeStep( string query, Type s [DataRow( "$[-113667776004:2]", typeof( JsonNode ) )] public void ArraySliceWithLargeNumberForStart( string query, Type sourceType ) { - //consensus: ["first", "second"] + // consensus: ["first", "second"] const string json = """ [ @@ -219,13 +219,13 @@ public void ArraySliceWithLargeNumberForStart( string query, Type sourceType ) "fifth" ] """; - var source = GetDocumentProxyFromSource( sourceType, json ); + var source = GetDocumentFromSource( sourceType, json ); var matches = source.Select( query ); var expected = new[] { - source.GetPropertyFromPath("$[0]"), - source.GetPropertyFromPath("$[1]") + source.FromJsonPathPointer("$[0]"), + source.FromJsonPathPointer("$[1]") }; Assert.IsTrue( expected.SequenceEqual( matches ) ); @@ -236,8 +236,8 @@ public void ArraySliceWithLargeNumberForStart( string query, Type sourceType ) [DataRow( "$[113667776004:2:-1]", typeof( JsonNode ) )] public void ArraySliceWithLargeNumberForStartAndNegativeStep( string query, Type sourceType ) { - //consensus: [] //partial - //deviation: ["fifth","forth"] //rfc + // rfc: ["fifth","forth"] + // consensus: [] partial const string json = """ [ @@ -248,13 +248,13 @@ public void ArraySliceWithLargeNumberForStartAndNegativeStep( string query, Type "fifth" ] """; - var source = GetDocumentProxyFromSource( sourceType, json ); + var source = GetDocumentFromSource( sourceType, json ); var matches = source.Select( query ); var expected = new[] { - source.GetPropertyFromPath("$[4]"), - source.GetPropertyFromPath("$[3]") + source.FromJsonPathPointer("$[4]"), + source.FromJsonPathPointer("$[3]") }; Assert.IsTrue( expected.SequenceEqual( matches ) ); @@ -265,7 +265,7 @@ public void ArraySliceWithLargeNumberForStartAndNegativeStep( string query, Type [DataRow( "$[-4:-5]", typeof( JsonNode ) )] public void ArraySliceWithNegativeStartAndEndAndRangeOfNegative1( string query, Type sourceType ) { - //consensus: [] + // consensus: [] const string json = """ [ @@ -277,10 +277,10 @@ public void ArraySliceWithNegativeStartAndEndAndRangeOfNegative1( string query, "nice" ] """; - var source = GetDocumentProxyFromSource( sourceType, json ); + var source = GetDocumentFromSource( sourceType, json ); var matches = source.Select( query ); - var expected = source.ArrayEmpty; + var expected = Enumerable.Empty(); Assert.IsTrue( expected.SequenceEqual( matches ) ); } @@ -290,7 +290,7 @@ public void ArraySliceWithNegativeStartAndEndAndRangeOfNegative1( string query, [DataRow( "$[-4:-4]", typeof( JsonNode ) )] public void ArraySliceWithNegativeStartAndEndAndRangeOf0( string query, Type sourceType ) { - //consensus: [] + // consensus: [] const string json = """ [ @@ -302,10 +302,10 @@ public void ArraySliceWithNegativeStartAndEndAndRangeOf0( string query, Type sou "nice" ] """; - var source = GetDocumentProxyFromSource( sourceType, json ); + var source = GetDocumentFromSource( sourceType, json ); var matches = source.Select( query ); - var expected = source.ArrayEmpty; + var expected = Enumerable.Empty(); Assert.IsTrue( expected.SequenceEqual( matches ) ); } @@ -315,7 +315,7 @@ public void ArraySliceWithNegativeStartAndEndAndRangeOf0( string query, Type sou [DataRow( "$[-4:-3]", typeof( JsonNode ) )] public void ArraySliceWithNegativeStartAndEndAndRangeOf1( string query, Type sourceType ) { - //consensus: [4] + // consensus: [4] const string json = """ [ @@ -327,12 +327,12 @@ public void ArraySliceWithNegativeStartAndEndAndRangeOf1( string query, Type sou "nice" ] """; - var source = GetDocumentProxyFromSource( sourceType, json ); + var source = GetDocumentFromSource( sourceType, json ); var matches = source.Select( query ); var expected = new[] { - source.GetPropertyFromPath("$[2]") + source.FromJsonPathPointer("$[2]") }; @@ -345,7 +345,7 @@ public void ArraySliceWithNegativeStartAndEndAndRangeOf1( string query, Type sou [DataRow( "$[-4:1]", typeof( JsonNode ) )] public void ArraySliceWithNegativeStartAndPositiveEndAndRangeOfNegative1( string query, Type sourceType ) { - //consensus: [] + // consensus: [] const string json = """ [ @@ -357,10 +357,10 @@ public void ArraySliceWithNegativeStartAndPositiveEndAndRangeOfNegative1( string "nice" ] """; - var source = GetDocumentProxyFromSource( sourceType, json ); + var source = GetDocumentFromSource( sourceType, json ); var matches = source.Select( query ); - var expected = source.ArrayEmpty; + var expected = Enumerable.Empty(); Assert.IsTrue( expected.SequenceEqual( matches ) ); } @@ -370,7 +370,7 @@ public void ArraySliceWithNegativeStartAndPositiveEndAndRangeOfNegative1( string [DataRow( "$[-4:2]", typeof( JsonNode ) )] public void ArraySliceWithNegativeStartAndPositiveEndAndRangeOf0( string query, Type sourceType ) { - //consensus: [] + // consensus: [] const string json = """ [ @@ -382,10 +382,10 @@ public void ArraySliceWithNegativeStartAndPositiveEndAndRangeOf0( string query, "nice" ] """; - var source = GetDocumentProxyFromSource( sourceType, json ); + var source = GetDocumentFromSource( sourceType, json ); var matches = source.Select( query ); - var expected = source.ArrayEmpty; + var expected = Enumerable.Empty(); Assert.IsTrue( expected.SequenceEqual( matches ) ); } @@ -395,7 +395,7 @@ public void ArraySliceWithNegativeStartAndPositiveEndAndRangeOf0( string query, [DataRow( "$[-4:3]", typeof( JsonNode ) )] public void ArraySliceWithNegativeStartAndPositiveEndAndRangeOf1( string query, Type sourceType ) { - //consensus: [4] + // consensus: [4] const string json = """ [ @@ -407,12 +407,12 @@ public void ArraySliceWithNegativeStartAndPositiveEndAndRangeOf1( string query, "nice" ] """; - var source = GetDocumentProxyFromSource( sourceType, json ); + var source = GetDocumentFromSource( sourceType, json ); var matches = source.Select( query ); var expected = new[] { - source.GetPropertyFromPath("$[2]") + source.FromJsonPathPointer("$[2]") }; Assert.IsTrue( expected.SequenceEqual( matches ) ); @@ -423,8 +423,8 @@ public void ArraySliceWithNegativeStartAndPositiveEndAndRangeOf1( string query, [DataRow( "$[3:0:-2]", typeof( JsonNode ) )] public void ArraySliceWithNegativeStep( string query, Type sourceType ) { - //consensus: //none - //deviation: ["forth","second"] //rfc + // rfc: ["forth","second"] + // consensus: none const string json = """ [ @@ -435,13 +435,13 @@ public void ArraySliceWithNegativeStep( string query, Type sourceType ) "fifth" ] """; - var source = GetDocumentProxyFromSource( sourceType, json ); + var source = GetDocumentFromSource( sourceType, json ); var matches = source.Select( query ); var expected = new[] { - source.GetPropertyFromPath("$[3]"), - source.GetPropertyFromPath("$[1]") + source.FromJsonPathPointer("$[3]"), + source.FromJsonPathPointer("$[1]") }; Assert.IsTrue( expected.SequenceEqual( matches ) ); @@ -452,8 +452,8 @@ public void ArraySliceWithNegativeStep( string query, Type sourceType ) [DataRow( "$[0:3:-2]", typeof( JsonNode ) )] public void ArraySliceWithNegativeStepAndStartGreaterThanEnd( string query, Type sourceType ) { - //consensus: //none - //deviation: [] //rfc + // rfc: [] + // consensus: none const string json = """ [ @@ -464,10 +464,10 @@ public void ArraySliceWithNegativeStepAndStartGreaterThanEnd( string query, Type "fifth" ] """; - var source = GetDocumentProxyFromSource( sourceType, json ); + var source = GetDocumentFromSource( sourceType, json ); var matches = source.Select( query ); - var expected = source.ArrayEmpty; + var expected = Enumerable.Empty(); Assert.IsTrue( expected.SequenceEqual( matches ) ); } @@ -477,8 +477,8 @@ public void ArraySliceWithNegativeStepAndStartGreaterThanEnd( string query, Type [DataRow( "$[7:3:-1]", typeof( JsonNode ) )] public void ArraySliceWithNegativeStepOnPartiallyOverlappingArray( string query, Type sourceType ) { - //consensus: //none - //deviation: ["fifth"] //rfc + // rfc: ["fifth"] + // consensus: none const string json = """ [ @@ -489,12 +489,12 @@ public void ArraySliceWithNegativeStepOnPartiallyOverlappingArray( string query, "fifth" ] """; - var source = GetDocumentProxyFromSource( sourceType, json ); + var source = GetDocumentFromSource( sourceType, json ); var matches = source.Select( query ); var expected = new[] { - source.GetPropertyFromPath("$[4]") + source.FromJsonPathPointer("$[4]") }; Assert.IsTrue( expected.SequenceEqual( matches ) ); @@ -505,8 +505,8 @@ public void ArraySliceWithNegativeStepOnPartiallyOverlappingArray( string query, [DataRow( "$[::-2]", typeof( JsonNode ) )] public void ArraySliceWithNegativeStepOnly( string query, Type sourceType ) { - //consensus: //none - //deviation: ["fifth","third","first"] //rfc + // consensus: none + // rfc: ["fifth","third","first"] //rfc const string json = """ [ @@ -517,14 +517,14 @@ public void ArraySliceWithNegativeStepOnly( string query, Type sourceType ) "fifth" ] """; - var source = GetDocumentProxyFromSource( sourceType, json ); + var source = GetDocumentFromSource( sourceType, json ); var matches = source.Select( query ); var expected = new[] { - source.GetPropertyFromPath("$[4]"), - source.GetPropertyFromPath("$[2]"), - source.GetPropertyFromPath("$[0]") + source.FromJsonPathPointer("$[4]"), + source.FromJsonPathPointer("$[2]"), + source.FromJsonPathPointer("$[0]") }; Assert.IsTrue( expected.SequenceEqual( matches ) ); @@ -535,7 +535,7 @@ public void ArraySliceWithNegativeStepOnly( string query, Type sourceType ) [DataRow( "$[1:]", typeof( JsonNode ) )] public void ArraySliceWithOpenEnd( string query, Type sourceType ) { - //consensus: ["second", "third", "forth", "fifth"] + // consensus: ["second", "third", "forth", "fifth"] const string json = """ [ @@ -546,15 +546,15 @@ public void ArraySliceWithOpenEnd( string query, Type sourceType ) "fifth" ] """; - var source = GetDocumentProxyFromSource( sourceType, json ); + var source = GetDocumentFromSource( sourceType, json ); var matches = source.Select( query ); var expected = new[] { - source.GetPropertyFromPath("$[1]"), - source.GetPropertyFromPath("$[2]"), - source.GetPropertyFromPath("$[3]"), - source.GetPropertyFromPath("$[4]") + source.FromJsonPathPointer("$[1]"), + source.FromJsonPathPointer("$[2]"), + source.FromJsonPathPointer("$[3]"), + source.FromJsonPathPointer("$[4]") }; Assert.IsTrue( expected.SequenceEqual( matches ) ); @@ -565,8 +565,8 @@ public void ArraySliceWithOpenEnd( string query, Type sourceType ) [DataRow( "$[3::-1]", typeof( JsonNode ) )] public void ArraySliceWithOpenEndAndNegativeStep( string query, Type sourceType ) { - //consensus: //none - //deviation: ["forth","third","second","first"] //rfc + // rfc: ["forth","third","second","first"] + // consensus: none const string json = """ [ @@ -577,15 +577,15 @@ public void ArraySliceWithOpenEndAndNegativeStep( string query, Type sourceType "fifth" ] """; - var source = GetDocumentProxyFromSource( sourceType, json ); + var source = GetDocumentFromSource( sourceType, json ); var matches = source.Select( query ); var expected = new[] { - source.GetPropertyFromPath("$[3]"), - source.GetPropertyFromPath("$[2]"), - source.GetPropertyFromPath("$[1]"), - source.GetPropertyFromPath("$[0]") + source.FromJsonPathPointer("$[3]"), + source.FromJsonPathPointer("$[2]"), + source.FromJsonPathPointer("$[1]"), + source.FromJsonPathPointer("$[0]") }; Assert.IsTrue( expected.SequenceEqual( matches ) ); @@ -596,7 +596,7 @@ public void ArraySliceWithOpenEndAndNegativeStep( string query, Type sourceType [DataRow( "$[:2]", typeof( JsonNode ) )] public void ArraySliceWithOpenStart( string query, Type sourceType ) { - //consensus: ["first", "second"] + // consensus: ["first", "second"] const string json = """ [ @@ -607,13 +607,13 @@ public void ArraySliceWithOpenStart( string query, Type sourceType ) "fifth" ] """; - var source = GetDocumentProxyFromSource( sourceType, json ); + var source = GetDocumentFromSource( sourceType, json ); var matches = source.Select( query ); var expected = new[] { - source.GetPropertyFromPath("$[0]"), - source.GetPropertyFromPath("$[1]") + source.FromJsonPathPointer("$[0]"), + source.FromJsonPathPointer("$[1]") }; Assert.IsTrue( expected.SequenceEqual( matches ) ); @@ -624,7 +624,7 @@ public void ArraySliceWithOpenStart( string query, Type sourceType ) [DataRow( "$[::]", typeof( JsonNode ) )] public void ArraySliceWithOpenStartAndEndAndStepEmpty( string query, Type sourceType ) { - //consensus: ["first", "second"] + // consensus: ["first", "second"] const string json = """ [ @@ -632,13 +632,13 @@ public void ArraySliceWithOpenStartAndEndAndStepEmpty( string query, Type source "second" ] """; - var source = GetDocumentProxyFromSource( sourceType, json ); + var source = GetDocumentFromSource( sourceType, json ); var matches = source.Select( query ); var expected = new[] { - source.GetPropertyFromPath("$[0]"), - source.GetPropertyFromPath("$[1]") + source.FromJsonPathPointer("$[0]"), + source.FromJsonPathPointer("$[1]") }; Assert.IsTrue( expected.SequenceEqual( matches ) ); @@ -649,7 +649,7 @@ public void ArraySliceWithOpenStartAndEndAndStepEmpty( string query, Type source [DataRow( "$[:]", typeof( JsonNode ) )] public void ArraySliceWithOpenStartAndEndOnObject( string query, Type sourceType ) { - //consensus: [] + // consensus: [] const string json = """ { @@ -657,10 +657,10 @@ public void ArraySliceWithOpenStartAndEndOnObject( string query, Type sourceType "more": "string" } """; - var source = GetDocumentProxyFromSource( sourceType, json ); + var source = GetDocumentFromSource( sourceType, json ); var matches = source.Select( query ); - var expected = source.ArrayEmpty; + var expected = Enumerable.Empty(); Assert.IsTrue( expected.SequenceEqual( matches ) ); } @@ -670,8 +670,8 @@ public void ArraySliceWithOpenStartAndEndOnObject( string query, Type sourceType [DataRow( "$[:2:-1]", typeof( JsonNode ) )] public void ArraySliceWithOpenStartAndNegativeStep( string query, Type sourceType ) { - //consensus: //none - //deviation: ["fifth","forth"] //rfc + // rfc: ["fifth","forth"] + // consensus: none const string json = """ [ @@ -682,13 +682,13 @@ public void ArraySliceWithOpenStartAndNegativeStep( string query, Type sourceTyp "fifth" ] """; - var source = GetDocumentProxyFromSource( sourceType, json ); + var source = GetDocumentFromSource( sourceType, json ); var matches = source.Select( query ); var expected = new[] { - source.GetPropertyFromPath("$[4]"), - source.GetPropertyFromPath("$[3]") + source.FromJsonPathPointer("$[4]"), + source.FromJsonPathPointer("$[3]") }; Assert.IsTrue( expected.SequenceEqual( matches ) ); @@ -699,7 +699,7 @@ public void ArraySliceWithOpenStartAndNegativeStep( string query, Type sourceTyp [DataRow( "$[3:-4]", typeof( JsonNode ) )] public void ArraySliceWithPositiveStartAndNegativeEndAndRangeOfNegative1( string query, Type sourceType ) { - //consensus: [] + // consensus: [] const string json = """ [ @@ -711,10 +711,10 @@ public void ArraySliceWithPositiveStartAndNegativeEndAndRangeOfNegative1( string "nice" ] """; - var source = GetDocumentProxyFromSource( sourceType, json ); + var source = GetDocumentFromSource( sourceType, json ); var matches = source.Select( query ); - var expected = source.ArrayEmpty; + var expected = Enumerable.Empty(); Assert.IsTrue( expected.SequenceEqual( matches ) ); } @@ -724,7 +724,7 @@ public void ArraySliceWithPositiveStartAndNegativeEndAndRangeOfNegative1( string [DataRow( "$[3:-3]", typeof( JsonNode ) )] public void ArraySliceWithPositiveStartAndNegativeEndAndRangeOf0( string query, Type sourceType ) { - //consensus: [] + // consensus: [] const string json = """ [ @@ -736,10 +736,10 @@ public void ArraySliceWithPositiveStartAndNegativeEndAndRangeOf0( string query, "nice" ] """; - var source = GetDocumentProxyFromSource( sourceType, json ); + var source = GetDocumentFromSource( sourceType, json ); var matches = source.Select( query ); - var expected = source.ArrayEmpty; + var expected = Enumerable.Empty(); Assert.IsTrue( expected.SequenceEqual( matches ) ); } @@ -749,7 +749,7 @@ public void ArraySliceWithPositiveStartAndNegativeEndAndRangeOf0( string query, [DataRow( "$[3:-2]", typeof( JsonNode ) )] public void ArraySliceWithPositiveStartAndNegativeEndAndRangeOf1( string query, Type sourceType ) { - //consensus: [5] + // consensus: [5] const string json = """ [ @@ -761,12 +761,12 @@ public void ArraySliceWithPositiveStartAndNegativeEndAndRangeOf1( string query, "nice" ] """; - var source = GetDocumentProxyFromSource( sourceType, json ); + var source = GetDocumentFromSource( sourceType, json ); var matches = source.Select( query ); var expected = new[] { - source.GetPropertyFromPath("$[3]") + source.FromJsonPathPointer("$[3]") }; Assert.IsTrue( expected.SequenceEqual( matches ) ); @@ -777,7 +777,7 @@ public void ArraySliceWithPositiveStartAndNegativeEndAndRangeOf1( string query, [DataRow( "$[2:1]", typeof( JsonNode ) )] public void ArraySliceWithRangeOfNegative1( string query, Type sourceType ) { - //consensus: [] + // consensus: [] const string json = """ [ @@ -787,10 +787,10 @@ public void ArraySliceWithRangeOfNegative1( string query, Type sourceType ) "forth" ] """; - var source = GetDocumentProxyFromSource( sourceType, json ); + var source = GetDocumentFromSource( sourceType, json ); var matches = source.Select( query ); - var expected = source.ArrayEmpty; + var expected = Enumerable.Empty(); Assert.IsTrue( expected.SequenceEqual( matches ) ); } @@ -800,7 +800,7 @@ public void ArraySliceWithRangeOfNegative1( string query, Type sourceType ) [DataRow( "$[0:0]", typeof( JsonNode ) )] public void ArraySliceWithRangeOf0( string query, Type sourceType ) { - //consensus: [] + // consensus: [] const string json = """ [ @@ -808,10 +808,10 @@ public void ArraySliceWithRangeOf0( string query, Type sourceType ) "second" ] """; - var source = GetDocumentProxyFromSource( sourceType, json ); + var source = GetDocumentFromSource( sourceType, json ); var matches = source.Select( query ); - var expected = source.ArrayEmpty; + var expected = Enumerable.Empty(); Assert.IsTrue( expected.SequenceEqual( matches ) ); } @@ -821,7 +821,7 @@ public void ArraySliceWithRangeOf0( string query, Type sourceType ) [DataRow( "$[0:1]", typeof( JsonNode ) )] public void ArraySliceWithRangeOf1( string query, Type sourceType ) { - //consensus: ["first"] + // consensus: ["first"] const string json = """ [ @@ -829,12 +829,12 @@ public void ArraySliceWithRangeOf1( string query, Type sourceType ) "second" ] """; - var source = GetDocumentProxyFromSource( sourceType, json ); + var source = GetDocumentFromSource( sourceType, json ); var matches = source.Select( query ); var expected = new[] { - source.GetPropertyFromPath("$[0]") + source.FromJsonPathPointer("$[0]") }; Assert.IsTrue( expected.SequenceEqual( matches ) ); @@ -845,7 +845,7 @@ public void ArraySliceWithRangeOf1( string query, Type sourceType ) [DataRow( "$[-1:]", typeof( JsonNode ) )] public void ArraySliceWithStartNegative1AndOpenEnd( string query, Type sourceType ) { - //consensus: ["third"] + // consensus: ["third"] const string json = """ [ @@ -854,12 +854,12 @@ public void ArraySliceWithStartNegative1AndOpenEnd( string query, Type sourceTyp "third" ] """; - var source = GetDocumentProxyFromSource( sourceType, json ); + var source = GetDocumentFromSource( sourceType, json ); var matches = source.Select( query ); var expected = new[] { - source.GetPropertyFromPath("$[2]") + source.FromJsonPathPointer("$[2]") }; Assert.IsTrue( expected.SequenceEqual( matches ) ); @@ -870,7 +870,7 @@ public void ArraySliceWithStartNegative1AndOpenEnd( string query, Type sourceTyp [DataRow( "$[-2:]", typeof( JsonNode ) )] public void ArraySliceWithStartMinus2AndOpenEnd( string query, Type sourceType ) { - //consensus: ["second", "third"] + // consensus: ["second", "third"] const string json = """ [ @@ -879,13 +879,13 @@ public void ArraySliceWithStartMinus2AndOpenEnd( string query, Type sourceType ) "third" ] """; - var source = GetDocumentProxyFromSource( sourceType, json ); + var source = GetDocumentFromSource( sourceType, json ); var matches = source.Select( query ); var expected = new[] { - source.GetPropertyFromPath("$[1]"), - source.GetPropertyFromPath("$[2]") + source.FromJsonPathPointer("$[1]"), + source.FromJsonPathPointer("$[2]") }; Assert.IsTrue( expected.SequenceEqual( matches ) ); @@ -896,7 +896,7 @@ public void ArraySliceWithStartMinus2AndOpenEnd( string query, Type sourceType ) [DataRow( "$[-4:]", typeof( JsonNode ) )] public void ArraySliceWithStartLargeNegativeNumberAndOpenEndOnShortArray( string query, Type sourceType ) { - //consensus: ["first", "second", "third"] + // consensus: ["first", "second", "third"] const string json = """ [ @@ -905,14 +905,14 @@ public void ArraySliceWithStartLargeNegativeNumberAndOpenEndOnShortArray( string "third" ] """; - var source = GetDocumentProxyFromSource( sourceType, json ); + var source = GetDocumentFromSource( sourceType, json ); var matches = source.Select( query ); var expected = new[] { - source.GetPropertyFromPath("$[0]"), - source.GetPropertyFromPath("$[1]"), - source.GetPropertyFromPath("$[2]") + source.FromJsonPathPointer("$[0]"), + source.FromJsonPathPointer("$[1]"), + source.FromJsonPathPointer("$[2]") }; Assert.IsTrue( expected.SequenceEqual( matches ) ); @@ -923,7 +923,7 @@ public void ArraySliceWithStartLargeNegativeNumberAndOpenEndOnShortArray( string [DataRow( "$[0:3:2]", typeof( JsonNode ) )] public void ArraySliceWithStep( string query, Type sourceType ) { - //consensus: ["first", "third"] + // consensus: ["first", "third"] const string json = """ [ @@ -934,13 +934,13 @@ public void ArraySliceWithStep( string query, Type sourceType ) "fifth" ] """; - var source = GetDocumentProxyFromSource( sourceType, json ); + var source = GetDocumentFromSource( sourceType, json ); var matches = source.Select( query ); var expected = new[] { - source.GetPropertyFromPath("$[0]"), - source.GetPropertyFromPath("$[2]") + source.FromJsonPathPointer("$[0]"), + source.FromJsonPathPointer("$[2]") }; Assert.IsTrue( expected.SequenceEqual( matches ) ); @@ -951,8 +951,8 @@ public void ArraySliceWithStep( string query, Type sourceType ) [DataRow( "$[0:3:0]", typeof( JsonNode ) )] public void ArraySliceWithStep0( string query, Type sourceType ) { - //consensus: //none - //deviation: [] //rfc + // rfc: [] + // consensus: none const string json = """ [ @@ -963,10 +963,10 @@ public void ArraySliceWithStep0( string query, Type sourceType ) "fifth" ] """; - var source = GetDocumentProxyFromSource( sourceType, json ); + var source = GetDocumentFromSource( sourceType, json ); var matches = source.Select( query ); - var expected = source.ArrayEmpty; + var expected = Enumerable.Empty(); Assert.IsTrue( expected.SequenceEqual( matches ) ); } @@ -976,7 +976,7 @@ public void ArraySliceWithStep0( string query, Type sourceType ) [DataRow( "$[0:3:1]", typeof( JsonNode ) )] public void ArraySliceWithStep1( string query, Type sourceType ) { - //consensus: ["first", "second", "third"] + // consensus: ["first", "second", "third"] const string json = """ [ @@ -987,14 +987,14 @@ public void ArraySliceWithStep1( string query, Type sourceType ) "fifth" ] """; - var source = GetDocumentProxyFromSource( sourceType, json ); + var source = GetDocumentFromSource( sourceType, json ); var matches = source.Select( query ); var expected = new[] { - source.GetPropertyFromPath("$[0]"), - source.GetPropertyFromPath("$[1]"), - source.GetPropertyFromPath("$[2]") + source.FromJsonPathPointer("$[0]"), + source.FromJsonPathPointer("$[1]"), + source.FromJsonPathPointer("$[2]") }; Assert.IsTrue( expected.SequenceEqual( matches ) ); @@ -1005,16 +1005,16 @@ public void ArraySliceWithStep1( string query, Type sourceType ) [DataRow( "$[010:024:010]", typeof( JsonNode ) )] public void ArraySliceWithStepAndLeadingZeros( string query, Type sourceType ) { - //consensus: [10, 20] + // 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 = GetDocumentProxyFromSource( sourceType, json ); + var source = GetDocumentFromSource( sourceType, json ); var matches = source.Select( query ); var expected = new[] { - source.GetPropertyFromPath("$[10]"), - source.GetPropertyFromPath("$[20]") + source.FromJsonPathPointer("$[10]"), + source.FromJsonPathPointer("$[20]") }; Assert.IsTrue( expected.SequenceEqual( matches ) ); @@ -1025,7 +1025,7 @@ public void ArraySliceWithStepAndLeadingZeros( string query, Type sourceType ) [DataRow( "$[0:4:2]", typeof( JsonNode ) )] public void ArraySliceWithStepButEndNotAligned( string query, Type sourceType ) { - //consensus: ["first", "third"] + // consensus: ["first", "third"] const string json = """ [ @@ -1036,13 +1036,13 @@ public void ArraySliceWithStepButEndNotAligned( string query, Type sourceType ) "fifth" ] """; - var source = GetDocumentProxyFromSource( sourceType, json ); + var source = GetDocumentFromSource( sourceType, json ); var matches = source.Select( query ); var expected = new[] { - source.GetPropertyFromPath("$[0]"), - source.GetPropertyFromPath("$[2]") + source.FromJsonPathPointer("$[0]"), + source.FromJsonPathPointer("$[2]") }; Assert.IsTrue( expected.SequenceEqual( matches ) ); @@ -1053,7 +1053,7 @@ public void ArraySliceWithStepButEndNotAligned( string query, Type sourceType ) [DataRow( "$[1:3:]", typeof( JsonNode ) )] public void ArraySliceWithStepEmpty( string query, Type sourceType ) { - //consensus: ["second", "third"] + // consensus: ["second", "third"] const string json = """ [ @@ -1064,13 +1064,13 @@ public void ArraySliceWithStepEmpty( string query, Type sourceType ) "fifth" ] """; - var source = GetDocumentProxyFromSource( sourceType, json ); + var source = GetDocumentFromSource( sourceType, json ); var matches = source.Select( query ); var expected = new[] { - source.GetPropertyFromPath("$[1]"), - source.GetPropertyFromPath("$[2]") + source.FromJsonPathPointer("$[1]"), + source.FromJsonPathPointer("$[2]") }; Assert.IsTrue( expected.SequenceEqual( matches ) ); diff --git a/test/Hyperbee.Json.Tests/Query/JsonPathBookstoreSelectPathTests.cs b/test/Hyperbee.Json.Tests/Query/JsonPathBookstoreSelectPathTests.cs new file mode 100644 index 00000000..65376c3e --- /dev/null +++ b/test/Hyperbee.Json.Tests/Query/JsonPathBookstoreSelectPathTests.cs @@ -0,0 +1,266 @@ +using System.Linq; +using System.Text.Json; +using Hyperbee.Json.Extensions; +using Hyperbee.Json.Tests.TestSupport; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Hyperbee.Json.Tests.Query; + +[TestClass] +public class JsonPathBookstoreSelectPathTests : JsonTestBase +{ + [DataTestMethod] + [DataRow( "$.store.book[*].author" )] + public void TheAuthorsOfAllBooksInTheStore( string query ) + { + var source = GetDocument(); + var matches = source.SelectPath( query ).ToList(); + + var expected = new[] + { + PathNodePair(source, "$.store.book[0].author"), + PathNodePair(source, "$.store.book[1].author"), + PathNodePair(source, "$.store.book[2].author"), + PathNodePair(source, "$.store.book[3].author") + }; + + Assert.IsTrue( expected.Select( e => e.Node ).SequenceEqual( matches.Select( x => x.Node ) ) ); + Assert.IsTrue( expected.Select( e => e.Path ).SequenceEqual( matches.Select( x => x.Path ) ) ); + } + + [DataTestMethod] + [DataRow( "$..author" )] + public void AllAuthors( string query ) + { + var source = GetDocument(); + var matches = source.SelectPath( query ).ToList(); + + var expected = new[] + { + PathNodePair(source, "$.store.book[0].author"), + PathNodePair(source, "$.store.book[1].author"), + PathNodePair(source, "$.store.book[2].author"), + PathNodePair(source, "$.store.book[3].author") + }; + + Assert.IsTrue( expected.Select( e => e.Node ).SequenceEqual( matches.Select( x => x.Node ) ) ); + Assert.IsTrue( expected.Select( e => e.Path ).SequenceEqual( matches.Select( x => x.Path ) ) ); + } + + [DataTestMethod] + [DataRow( "$.store.*" )] + public void AllThingsInStoreWhichAreSomeBooksAndOneRedBicycle( string query ) + { + var source = GetDocument(); + var matches = source.SelectPath( query ).ToList(); + + var expected = new[] + { + PathNodePair(source, "$.store.book"), + PathNodePair(source, "$.store.bicycle") + }; + + Assert.IsTrue( expected.Select( e => e.Node ).SequenceEqual( matches.Select( x => x.Node ) ) ); + Assert.IsTrue( expected.Select( e => e.Path ).SequenceEqual( matches.Select( x => x.Path ) ) ); + } + + [DataTestMethod] + [DataRow( "$.store..price" )] + public void ThePriceOfEverythingInTheStore( string query ) + { + var source = GetDocument(); + var matches = source.SelectPath( query ).ToList(); + + var expected = new[] + { + PathNodePair(source, "$.store.book[0].price"), + PathNodePair(source, "$.store.book[1].price"), + PathNodePair(source, "$.store.book[2].price"), + PathNodePair(source, "$.store.book[3].price"), + PathNodePair(source, "$.store.bicycle.price") + }; + + Assert.IsTrue( expected.Select( e => e.Node ).SequenceEqual( matches.Select( x => x.Node ) ) ); + Assert.IsTrue( expected.Select( e => e.Path ).SequenceEqual( matches.Select( x => x.Path ) ) ); + } + + [DataTestMethod] + [DataRow( "$..book[2]" )] + public void TheThirdBook( string query ) + { + var source = GetDocument(); + var match = source.SelectPath( query ).ToList(); + + var expected = PathNodePair( source, "$.store.book[2]" ); + + Assert.IsTrue( match.Count == 1 ); + Assert.AreEqual( expected.Node, match[0].Node ); + Assert.AreEqual( expected.Path, match[0].Path ); + } + + [DataTestMethod] + [DataRow( "$..book[-1:]" )] + public void TheLastBookInOrder( string query ) + { + var source = GetDocument(); + var match = source.SelectPath( query ).Single(); + + var expected = PathNodePair( source, "$.store.book[3]" ); + + Assert.AreEqual( expected.Node, match.Node ); + Assert.AreEqual( expected.Path, match.Path ); + } + + [DataTestMethod] + [DataRow( "$..book[:2]" )] + [DataRow( "$..book[0,1]" )] + [DataRow( "$.store.book[0,1]" )] + public void TheFirstTwoBooks( string query ) + { + var source = GetDocument(); + var matches = source.SelectPath( query ).ToList(); + + var expected = new[] + { + PathNodePair(source, "$.store.book[0]"), + PathNodePair(source, "$.store.book[1]") + }; + + Assert.IsTrue( expected.Select( e => e.Node ).SequenceEqual( matches.Select( x => x.Node ) ) ); + Assert.IsTrue( expected.Select( e => e.Path ).SequenceEqual( matches.Select( x => x.Path ) ) ); + } + + [DataTestMethod] + [DataRow( "$..book['category','author']" )] + public void TheCategoriesAndAuthorsOfAllBooks( string query ) + { + var source = GetDocument(); + var matches = source.SelectPath( query ).ToList(); + + var expected = new[] + { + PathNodePair(source, "$.store.book[0].category"), + PathNodePair(source, "$.store.book[1].category"), + PathNodePair(source, "$.store.book[2].category"), + PathNodePair(source, "$.store.book[3].category"), + PathNodePair(source, "$.store.book[0].author"), + PathNodePair(source, "$.store.book[1].author"), + PathNodePair(source, "$.store.book[2].author"), + PathNodePair(source, "$.store.book[3].author") + }; + + Assert.IsTrue( expected.Select( e => e.Node ).SequenceEqual( matches.Select( x => x.Node ) ) ); + Assert.IsTrue( expected.Select( e => e.Path ).SequenceEqual( matches.Select( x => x.Path ) ) ); + } + + [DataTestMethod] + [DataRow( "$..book[?@.isbn]" )] + [DataRow( "$..book[?(@.isbn)]" )] + public void FilterAllBooksWithIsbnNumber( string query ) + { + var source = GetDocument(); + var matches = source.SelectPath( query ).ToList(); + + var expected = new[] + { + PathNodePair(source, "$.store.book[2]"), + PathNodePair(source, "$.store.book[3]") + }; + + Assert.IsTrue( expected.Select( e => e.Node ).SequenceEqual( matches.Select( x => x.Node ) ) ); + Assert.IsTrue( expected.Select( e => e.Path ).SequenceEqual( matches.Select( x => x.Path ) ) ); + } + + [DataTestMethod] + [DataRow( "$..book[?(@.price<10)]" )] + [DataRow( "$..book[?@.price<10]" )] + public void FilterAllBooksCheaperThan10( string query ) + { + var source = GetDocument(); + var matches = source.SelectPath( query ).ToList(); + + var expected = new[] + { + PathNodePair(source, "$.store.book[0]"), + PathNodePair(source, "$.store.book[2]") + }; + + Assert.IsTrue( expected.Select( e => e.Node ).SequenceEqual( matches.Select( x => x.Node ) ) ); + Assert.IsTrue( expected.Select( e => e.Path ).SequenceEqual( matches.Select( x => x.Path ) ) ); + } + + [DataTestMethod] + [DataRow( "$..*" )] + public void AllMembersOfJsonStructure( string query ) + { + var source = GetDocument(); + var matches = source.SelectPath( query ).ToList(); + + var expected = new[] + { + PathNodePair(source, "$.store"), + PathNodePair(source, "$.store.book"), + PathNodePair(source, "$.store.bicycle"), + PathNodePair(source, "$.store.book[0]"), + PathNodePair(source, "$.store.book[1]"), + PathNodePair(source, "$.store.book[2]"), + PathNodePair(source, "$.store.book[3]"), + PathNodePair(source, "$.store.book[0].category"), + PathNodePair(source, "$.store.book[0].author"), + PathNodePair(source, "$.store.book[0].title"), + PathNodePair(source, "$.store.book[0].price"), + PathNodePair(source, "$.store.book[1].category"), + PathNodePair(source, "$.store.book[1].author"), + PathNodePair(source, "$.store.book[1].title"), + PathNodePair(source, "$.store.book[1].price"), + PathNodePair(source, "$.store.book[2].category"), + PathNodePair(source, "$.store.book[2].author"), + PathNodePair(source, "$.store.book[2].title"), + PathNodePair(source, "$.store.book[2].isbn"), + PathNodePair(source, "$.store.book[2].price"), + PathNodePair(source, "$.store.book[3].category"), + PathNodePair(source, "$.store.book[3].author"), + PathNodePair(source, "$.store.book[3].title"), + PathNodePair(source, "$.store.book[3].isbn"), + PathNodePair(source, "$.store.book[3].price"), + PathNodePair(source, "$.store.bicycle.color"), + PathNodePair(source, "$.store.bicycle.price") + }; + + Assert.IsTrue( expected.Select( e => e.Node ).SequenceEqual( matches.Select( x => x.Node ) ) ); + Assert.IsTrue( expected.Select( e => e.Path ).SequenceEqual( matches.Select( x => x.Path ) ) ); + } + + [DataTestMethod] + [DataRow( @"$..book[?(@.price == 8.99 && @.category == ""fiction"")]" )] + [DataRow( @"$..book[?@.price == 8.99 && @.category == ""fiction""]" )] + public void FilterAllBooksUsingLogicalAndInScript( string query ) + { + var source = GetDocument(); + var match = source.SelectPath( query ).Single(); + + var expected = PathNodePair( source, "$.store.book[2]" ); + + Assert.AreEqual( expected.Node, match.Node ); + Assert.AreEqual( expected.Path, match.Path ); + } + + [DataTestMethod] + [DataRow( @"$..book[?@.price == 8.99 && (@.category == ""fiction"")]" )] + public void FilterWithUnevenParentheses( string query ) + { + var source = GetDocument(); + var match = source.SelectPath( query ).Single(); + + var expected = PathNodePair( source, "$.store.book[2]" ); + + Assert.AreEqual( expected.Node, match.Node ); + Assert.AreEqual( expected.Path, match.Path ); + } + + // Helper method to return a tuple of the path and the node at that path + private static (string Path, JsonElement Node) PathNodePair( JsonElement source, string path ) + { + return (path, source.FromJsonPathPointer( path )); + } +} diff --git a/test/Hyperbee.Json.Tests/Query/JsonPathBookstoreTests.cs b/test/Hyperbee.Json.Tests/Query/JsonPathBookstoreTests.cs index b70aa0ea..aab3384f 100644 --- a/test/Hyperbee.Json.Tests/Query/JsonPathBookstoreTests.cs +++ b/test/Hyperbee.Json.Tests/Query/JsonPathBookstoreTests.cs @@ -15,11 +15,11 @@ public class JsonPathBookstoreTests : JsonTestBase [DataRow( "$", typeof( JsonNode ) )] public void TheRootOfEverything( string query, Type sourceType ) { - var source = GetDocumentProxy( sourceType ); + var source = GetDocumentFromResource( sourceType ); var matches = source.Select( query ); var expected = new[] { - source.GetPropertyFromPath( "$" ) + source.FromJsonPathPointer( "$" ) }; Assert.IsTrue( expected.SequenceEqual( matches ) ); @@ -30,14 +30,14 @@ public void TheRootOfEverything( string query, Type sourceType ) [DataRow( "$.store.book[*].author", typeof( JsonNode ) )] public void TheAuthorsOfAllBooksInTheStore( string query, Type sourceType ) { - var source = GetDocumentProxy( sourceType ); + var source = GetDocumentFromResource( sourceType ); var matches = source.Select( query ); var expected = new[] { - source.GetPropertyFromPath( "$['store']['book'][0]['author']" ), - source.GetPropertyFromPath( "$['store']['book'][1]['author']" ), - source.GetPropertyFromPath( "$['store']['book'][2]['author']" ), - source.GetPropertyFromPath( "$['store']['book'][3]['author']" ) + source.FromJsonPathPointer( "$.store.book[0].author" ), + source.FromJsonPathPointer( "$.store.book[1].author" ), + source.FromJsonPathPointer( "$.store.book[2].author" ), + source.FromJsonPathPointer( "$.store.book[3].author" ) }; Assert.IsTrue( expected.SequenceEqual( matches ) ); @@ -48,14 +48,14 @@ public void TheAuthorsOfAllBooksInTheStore( string query, Type sourceType ) [DataRow( "$..author", typeof( JsonNode ) )] public void AllAuthors( string query, Type sourceType ) { - var source = GetDocumentProxy( sourceType ); + var source = GetDocumentFromResource( sourceType ); var matches = source.Select( query ); var expected = new[] { - source.GetPropertyFromPath( "$['store']['book'][0]['author']" ), - source.GetPropertyFromPath( "$['store']['book'][1]['author']" ), - source.GetPropertyFromPath( "$['store']['book'][2]['author']" ), - source.GetPropertyFromPath( "$['store']['book'][3]['author']" ) + source.FromJsonPathPointer( "$.store.book[0].author" ), + source.FromJsonPathPointer( "$.store.book[1].author" ), + source.FromJsonPathPointer( "$.store.book[2].author" ), + source.FromJsonPathPointer( "$.store.book[3].author" ) }; Assert.IsTrue( expected.SequenceEqual( matches ) ); @@ -66,12 +66,12 @@ public void AllAuthors( string query, Type sourceType ) [DataRow( "$.store.*", typeof( JsonNode ) )] public void AllThingsInStoreWhichAreSomeBooksAndOneRedBicycle( string query, Type sourceType ) { - var source = GetDocumentProxy( sourceType ); + var source = GetDocumentFromResource( sourceType ); var matches = source.Select( query ); var expected = new[] { - source.GetPropertyFromPath( "$['store']['book']" ), - source.GetPropertyFromPath( "$['store']['bicycle']" ) + source.FromJsonPathPointer( "$.store.book" ), + source.FromJsonPathPointer( "$.store.bicycle" ) }; Assert.IsTrue( expected.SequenceEqual( matches ) ); @@ -82,15 +82,15 @@ public void AllThingsInStoreWhichAreSomeBooksAndOneRedBicycle( string query, Typ [DataRow( "$.store..price", typeof( JsonNode ) )] public void ThePriceOfEverythingInTheStore( string query, Type sourceType ) { - var source = GetDocumentProxy( sourceType ); + var source = GetDocumentFromResource( sourceType ); var matches = source.Select( query ); var expected = new[] { - source.GetPropertyFromPath( "$['store']['book'][0]['price']" ), - source.GetPropertyFromPath( "$['store']['book'][1]['price']" ), - source.GetPropertyFromPath( "$['store']['book'][2]['price']" ), - source.GetPropertyFromPath( "$['store']['book'][3]['price']" ), - source.GetPropertyFromPath( "$['store']['bicycle']['price']" ) + source.FromJsonPathPointer( "$.store.book[0].price" ), + source.FromJsonPathPointer( "$.store.book[1].price" ), + source.FromJsonPathPointer( "$.store.book[2].price" ), + source.FromJsonPathPointer( "$.store.book[3].price" ), + source.FromJsonPathPointer( "$.store.bicycle.price" ) }; Assert.IsTrue( expected.SequenceEqual( matches ) ); @@ -101,9 +101,9 @@ public void ThePriceOfEverythingInTheStore( string query, Type sourceType ) [DataRow( "$..book[2]", typeof( JsonNode ) )] public void TheThirdBook( string query, Type sourceType ) { - var source = GetDocumentProxy( sourceType ); + var source = GetDocumentFromResource( sourceType ); var match = source.Select( query ).ToList(); - var expected = source.GetPropertyFromPath( "$['store']['book'][2]" ); + var expected = source.FromJsonPathPointer( "$.store.book[2]" ); Assert.IsTrue( match.Count == 1 ); Assert.AreEqual( expected, match[0] ); @@ -114,9 +114,9 @@ public void TheThirdBook( string query, Type sourceType ) [DataRow( "$..book[-1:]", typeof( JsonNode ) )] public void TheLastBookInOrder( string query, Type sourceType ) { - var source = GetDocumentProxy( sourceType ); + var source = GetDocumentFromResource( sourceType ); var match = source.Select( query ).Single(); - var expected = source.GetPropertyFromPath( "$['store']['book'][3]" ); + var expected = source.FromJsonPathPointer( "$.store.book[3]" ); Assert.AreEqual( expected, match ); } @@ -130,12 +130,12 @@ public void TheLastBookInOrder( string query, Type sourceType ) [DataRow( "$.store.book[0,1]", typeof( JsonNode ) )] public void TheFirstTwoBooks( string query, Type sourceType ) { - var source = GetDocumentProxy( sourceType ); + var source = GetDocumentFromResource( sourceType ); var matches = source.Select( query ); var expected = new[] { - source.GetPropertyFromPath( "$['store']['book'][0]" ), - source.GetPropertyFromPath( "$['store']['book'][1]" ) + source.FromJsonPathPointer( "$.store.book[0]" ), + source.FromJsonPathPointer( "$.store.book[1]" ) }; Assert.IsTrue( expected.SequenceEqual( matches ) ); @@ -146,18 +146,18 @@ public void TheFirstTwoBooks( string query, Type sourceType ) [DataRow( "$..book['category','author']", typeof( JsonNode ) )] public void TheCategoriesAndAuthorsOfAllBooks( string query, Type sourceType ) { - var source = GetDocumentProxy( sourceType ); + var source = GetDocumentFromResource( sourceType ); var matches = source.Select( query ); var expected = new[] { - source.GetPropertyFromPath( "$['store']['book'][0]['category']" ), - source.GetPropertyFromPath( "$['store']['book'][1]['category']" ), - source.GetPropertyFromPath( "$['store']['book'][2]['category']" ), - source.GetPropertyFromPath( "$['store']['book'][3]['category']" ), - source.GetPropertyFromPath( "$['store']['book'][0]['author']" ), - source.GetPropertyFromPath( "$['store']['book'][1]['author']" ), - source.GetPropertyFromPath( "$['store']['book'][2]['author']" ), - source.GetPropertyFromPath( "$['store']['book'][3]['author']" ) + source.FromJsonPathPointer( "$.store.book[0].category" ), + source.FromJsonPathPointer( "$.store.book[1].category" ), + source.FromJsonPathPointer( "$.store.book[2].category" ), + source.FromJsonPathPointer( "$.store.book[3].category" ), + source.FromJsonPathPointer( "$.store.book[0].author" ), + source.FromJsonPathPointer( "$.store.book[1].author" ), + source.FromJsonPathPointer( "$.store.book[2].author" ), + source.FromJsonPathPointer( "$.store.book[3].author" ) }; Assert.IsTrue( expected.SequenceEqual( matches ) ); @@ -170,12 +170,12 @@ public void TheCategoriesAndAuthorsOfAllBooks( string query, Type sourceType ) [DataRow( "$..book[?(@.isbn)]", typeof( JsonNode ) )] public void FilterAllBooksWithIsbnNumber( string query, Type sourceType ) { - var source = GetDocumentProxy( sourceType ); + var source = GetDocumentFromResource( sourceType ); var matches = source.Select( query ); var expected = new[] { - source.GetPropertyFromPath( "$['store']['book'][2]" ), - source.GetPropertyFromPath( "$['store']['book'][3]" ) + source.FromJsonPathPointer( "$.store.book[2]" ), + source.FromJsonPathPointer( "$.store.book[3]" ) }; Assert.IsTrue( expected.SequenceEqual( matches ) ); @@ -188,12 +188,12 @@ public void FilterAllBooksWithIsbnNumber( string query, Type sourceType ) [DataRow( "$..book[?@.price<10]", typeof( JsonNode ) )] public void FilterAllBooksCheaperThan10( string query, Type sourceType ) { - var source = GetDocumentProxy( sourceType ); + var source = GetDocumentFromResource( sourceType ); var matches = source.Select( query ); var expected = new[] { - source.GetPropertyFromPath( "$['store']['book'][0]" ), - source.GetPropertyFromPath( "$['store']['book'][2]" ) + source.FromJsonPathPointer( "$.store.book[0]" ), + source.FromJsonPathPointer( "$.store.book[2]" ) }; Assert.IsTrue( expected.SequenceEqual( matches ) ); @@ -204,37 +204,37 @@ public void FilterAllBooksCheaperThan10( string query, Type sourceType ) [DataRow( "$..*", typeof( JsonNode ) )] public void AllMembersOfJsonStructure( string query, Type sourceType ) { - var source = GetDocumentProxy( sourceType ); + var source = GetDocumentFromResource( sourceType ); var matches = source.Select( query ); var expected = new[] { - source.GetPropertyFromPath( "$['store']" ), - source.GetPropertyFromPath( "$['store']['book']" ), - source.GetPropertyFromPath( "$['store']['bicycle']" ), - source.GetPropertyFromPath( "$['store']['book'][0]" ), - source.GetPropertyFromPath( "$['store']['book'][1]" ), - source.GetPropertyFromPath( "$['store']['book'][2]" ), - source.GetPropertyFromPath( "$['store']['book'][3]" ), - source.GetPropertyFromPath( "$['store']['book'][0]['category']" ), - source.GetPropertyFromPath( "$['store']['book'][0]['author']" ), - source.GetPropertyFromPath( "$['store']['book'][0]['title']" ), - source.GetPropertyFromPath( "$['store']['book'][0]['price']" ), - source.GetPropertyFromPath( "$['store']['book'][1]['category']" ), - source.GetPropertyFromPath( "$['store']['book'][1]['author']" ), - source.GetPropertyFromPath( "$['store']['book'][1]['title']" ), - source.GetPropertyFromPath( "$['store']['book'][1]['price']" ), - source.GetPropertyFromPath( "$['store']['book'][2]['category']" ), - source.GetPropertyFromPath( "$['store']['book'][2]['author']" ), - source.GetPropertyFromPath( "$['store']['book'][2]['title']" ), - source.GetPropertyFromPath( "$['store']['book'][2]['isbn']" ), - source.GetPropertyFromPath( "$['store']['book'][2]['price']" ), - source.GetPropertyFromPath( "$['store']['book'][3]['category']" ), - source.GetPropertyFromPath( "$['store']['book'][3]['author']" ), - source.GetPropertyFromPath( "$['store']['book'][3]['title']" ), - source.GetPropertyFromPath( "$['store']['book'][3]['isbn']" ), - source.GetPropertyFromPath( "$['store']['book'][3]['price']" ), - source.GetPropertyFromPath( "$['store']['bicycle']['color']" ), - source.GetPropertyFromPath( "$['store']['bicycle']['price']" ) + source.FromJsonPathPointer( "$.store" ), + source.FromJsonPathPointer( "$.store.book" ), + source.FromJsonPathPointer( "$.store.bicycle" ), + source.FromJsonPathPointer( "$.store.book[0]" ), + source.FromJsonPathPointer( "$.store.book[1]" ), + source.FromJsonPathPointer( "$.store.book[2]" ), + source.FromJsonPathPointer( "$.store.book[3]" ), + source.FromJsonPathPointer( "$.store.book[0].category" ), + source.FromJsonPathPointer( "$.store.book[0].author" ), + source.FromJsonPathPointer( "$.store.book[0].title" ), + source.FromJsonPathPointer( "$.store.book[0].price" ), + source.FromJsonPathPointer( "$.store.book[1].category" ), + source.FromJsonPathPointer( "$.store.book[1].author" ), + source.FromJsonPathPointer( "$.store.book[1].title" ), + source.FromJsonPathPointer( "$.store.book[1].price" ), + source.FromJsonPathPointer( "$.store.book[2].category" ), + source.FromJsonPathPointer( "$.store.book[2].author" ), + source.FromJsonPathPointer( "$.store.book[2].title" ), + source.FromJsonPathPointer( "$.store.book[2].isbn" ), + source.FromJsonPathPointer( "$.store.book[2].price" ), + source.FromJsonPathPointer( "$.store.book[3].category" ), + source.FromJsonPathPointer( "$.store.book[3].author" ), + source.FromJsonPathPointer( "$.store.book[3].title" ), + source.FromJsonPathPointer( "$.store.book[3].isbn" ), + source.FromJsonPathPointer( "$.store.book[3].price" ), + source.FromJsonPathPointer( "$.store.bicycle.color" ), + source.FromJsonPathPointer( "$.store.bicycle.price" ) }; Assert.IsTrue( expected.SequenceEqual( matches ) ); @@ -247,9 +247,9 @@ public void AllMembersOfJsonStructure( string query, Type sourceType ) [DataRow( @"$..book[?@.price == 8.99 && @.category == ""fiction""]", typeof( JsonNode ) )] public void FilterAllBooksUsingLogicalAndInScript( string query, Type sourceType ) { - var source = GetDocumentProxy( sourceType ); + var source = GetDocumentFromResource( sourceType ); var match = source.Select( query ).Single(); - var expected = source.GetPropertyFromPath( "$['store']['book'][2]" ); + var expected = source.FromJsonPathPointer( "$.store.book[2]" ); Assert.AreEqual( expected, match ); } @@ -260,9 +260,9 @@ public void FilterAllBooksUsingLogicalAndInScript( string query, Type sourceType [DataRow( @"$..book[?@.price == 8.99 && (@.category == ""fiction"")]", typeof( JsonNode ) )] public void FilterWithUnevenParentheses( string query, Type sourceType ) { - var source = GetDocumentProxy( sourceType ); + var source = GetDocumentFromResource( sourceType ); var match = source.Select( query ).Single(); - var expected = source.GetPropertyFromPath( "$['store']['book'][2]" ); + var expected = source.FromJsonPathPointer( "$.store.book[2]" ); Assert.AreEqual( expected, match ); } diff --git a/test/Hyperbee.Json.Tests/Query/JsonPathBracketNotationTests.cs b/test/Hyperbee.Json.Tests/Query/JsonPathBracketNotationTests.cs index 4df94ec3..1951d70a 100644 --- a/test/Hyperbee.Json.Tests/Query/JsonPathBracketNotationTests.cs +++ b/test/Hyperbee.Json.Tests/Query/JsonPathBracketNotationTests.cs @@ -15,19 +15,19 @@ public class JsonPathBracketNotationTests : JsonTestBase [DataRow( "$['key']", typeof( JsonNode ) )] public void BracketNotation( string query, Type sourceType ) { - //consensus: ["value"] + // consensus: ["value"] const string json = """ { "key": "value" } """; - var source = GetDocumentProxyFromSource( sourceType, json ); + var source = GetDocumentFromSource( sourceType, json ); var matches = source.Select( query ); var expected = new[] { - source.GetPropertyFromPath("$['key']") + source.FromJsonPathPointer("$['key']") }; Assert.IsTrue( expected.SequenceEqual( matches ) ); @@ -38,43 +38,43 @@ public void BracketNotation( string query, Type sourceType ) [DataRow( "$..[0]", typeof( JsonNode ) )] public void BracketNotationAfterRecursiveDescent( string query, Type sourceType ) { - //consensus: ["deepest", "first nested", "first", "more", {"nested": ["deepest", "second"]}] - //deviation: consensus results/different order //rfc in selector order + // rfc: in selector order + // consensus: ["deepest", "first nested", "first", "more", {"nested": ["deepest", "second"]}] const string json = """ [ - "first", - { - "key": [ - "first nested", - { - "more": [ - { - "nested": [ - "deepest", - "second" - ] - }, - [ - "more", - "values" - ] - ] - } + "first", + { + "key": [ + "first nested", + { + "more": [ + { + "nested": [ + "deepest", + "second" + ] + }, + [ + "more", + "values" + ] ] - } + } + ] + } ] """; - var source = GetDocumentProxyFromSource( sourceType, json ); + var source = GetDocumentFromSource( sourceType, json ); var matches = source.Select( query ); var expected = new[] { - source.GetPropertyFromPath("$[0]"), - source.GetPropertyFromPath("$[1]['key'][0]"), - source.GetPropertyFromPath("$[1]['key'][1]['more'][0]"), - source.GetPropertyFromPath("$[1]['key'][1]['more'][0]['nested'][0]"), - source.GetPropertyFromPath("$[1]['key'][1]['more'][1][0]") + source.FromJsonPathPointer("$[0]"), + source.FromJsonPathPointer("$[1]['key'][0]"), + source.FromJsonPathPointer("$[1]['key'][1]['more'][0]"), + source.FromJsonPathPointer("$[1]['key'][1]['more'][0]['nested'][0]"), + source.FromJsonPathPointer("$[1]['key'][1]['more'][1][0]") }; Assert.IsTrue( expected.SequenceEqual( matches ) ); @@ -85,17 +85,17 @@ public void BracketNotationAfterRecursiveDescent( string query, Type sourceType [DataRow( "$['missing']", typeof( JsonNode ) )] public void BracketNotationOnObjectWithoutKey( string query, Type sourceType ) { - //consensus: [] + // consensus: [] const string json = """ { "key": "value" } """; - var source = GetDocumentProxyFromSource( sourceType, json ); + var source = GetDocumentFromSource( sourceType, json ); var matches = source.Select( query ); - var expected = source.ArrayEmpty; + var expected = Enumerable.Empty(); Assert.IsTrue( expected.SequenceEqual( matches ) ); } @@ -105,17 +105,17 @@ public void BracketNotationOnObjectWithoutKey( string query, Type sourceType ) [DataRow( "$['ü']", typeof( JsonNode ) )] public void BracketNotationWithNFCPathOnNFDKey( string query, Type sourceType ) { - //consensus: [] + // consensus: [] const string json = """ { "u\u0308": 42 } """; - var source = GetDocumentProxyFromSource( sourceType, json ); + var source = GetDocumentFromSource( sourceType, json ); var matches = source.Select( query ); - var expected = source.ArrayEmpty; + var expected = Enumerable.Empty(); Assert.IsTrue( expected.SequenceEqual( matches ) ); } @@ -125,8 +125,8 @@ public void BracketNotationWithNFCPathOnNFDKey( string query, Type sourceType ) [DataRow( "$['two.some']", typeof( JsonNode ) )] public void BracketNotationWithDot( string query, Type sourceType ) { - //consensus: //none - //deviation: "42" //support bracket notation on objects + // rfc: "42" // support bracket notation on objects + // consensus: none const string json = """ { @@ -140,7 +140,7 @@ public void BracketNotationWithDot( string query, Type sourceType ) "two.some": "42" } """; - var source = GetDocumentProxyFromSource( sourceType, json ); + var source = GetDocumentFromSource( sourceType, json ); var matches = source.Select( query ).ToList(); Assert.IsTrue( matches.Count == 1 ); @@ -152,14 +152,14 @@ public void BracketNotationWithDot( string query, Type sourceType ) [DataRow( "$[\"key\"]", typeof( JsonNode ) )] public void BracketNotationWithDoubleQuotes( string query, Type sourceType ) { - //consensus: ["value"] + // consensus: ["value"] const string json = """ { "key": "value" } """; - var source = GetDocumentProxyFromSource( sourceType, json ); + var source = GetDocumentFromSource( sourceType, json ); var matches = source.Select( query ).ToList(); Assert.IsTrue( matches.Count == 1 ); @@ -171,7 +171,7 @@ public void BracketNotationWithDoubleQuotes( string query, Type sourceType ) [DataRow( "$[]", typeof( JsonNode ) )] public void BracketNotationWithEmptyPath( string query, Type sourceType ) { - //consensus: NOT_SUPPORTED + // consensus: NOT_SUPPORTED const string json = """ { @@ -180,12 +180,11 @@ public void BracketNotationWithEmptyPath( string query, Type sourceType ) "\"\"": 222 } """; - var source = GetDocumentProxyFromSource( sourceType, json ); + var source = GetDocumentFromSource( sourceType, json ); Assert.ThrowsException( () => { - var _ = source.Select( query ).ToList(); - + _ = source.Select( query ).ToList(); }, "Invalid bracket expression syntax. Bracket expression cannot be empty." ); } @@ -194,7 +193,7 @@ public void BracketNotationWithEmptyPath( string query, Type sourceType ) [DataRow( "$['']", typeof( JsonNode ) )] public void BracketNotationWithEmptyString( string query, Type sourceType ) { - //consensus: [42] + // consensus: [42] const string json = """ { @@ -203,7 +202,7 @@ public void BracketNotationWithEmptyString( string query, Type sourceType ) "\"\"": 222 } """; - var source = GetDocumentProxyFromSource( sourceType, json ); + var source = GetDocumentFromSource( sourceType, json ); var matches = source.Select( query ).ToList(); Assert.IsTrue( matches.Count == 1 ); @@ -215,7 +214,7 @@ public void BracketNotationWithEmptyString( string query, Type sourceType ) [DataRow( "$[\"\"]", typeof( JsonNode ) )] public void BracketNotationWithEmptyStringDoubleQuoted( string query, Type sourceType ) { - //consensus: [42] + // consensus: [42] const string json = """ { @@ -224,7 +223,7 @@ public void BracketNotationWithEmptyStringDoubleQuoted( string query, Type sourc "\"\"": 222 } """; - var source = GetDocumentProxyFromSource( sourceType, json ); + var source = GetDocumentFromSource( sourceType, json ); var matches = source.Select( query ).ToList(); Assert.IsTrue( matches.Count == 1 ); @@ -236,15 +235,15 @@ public void BracketNotationWithEmptyStringDoubleQuoted( string query, Type sourc [DataRow( "$[-2]", typeof( JsonNode ) )] public void BracketNotationWithNegativeNumberOnShortArray( string query, Type sourceType ) { - //consensus: [] - //deviation: ["one element] //rfc (-2 => -2:1:1 => -1:1:1 [0]) + // rfc: ["one element] // (-2 => -2:1:1 => -1:1:1 [0]) + // consensus: [] const string json = """ [ "one element" ] """; - var source = GetDocumentProxyFromSource( sourceType, json ); + var source = GetDocumentFromSource( sourceType, json ); var matches = source.Select( query ).ToList(); @@ -257,7 +256,7 @@ public void BracketNotationWithNegativeNumberOnShortArray( string query, Type so [DataRow( "$[2]", typeof( JsonNode ) )] public void BracketNotationWithNumber( string query, Type sourceType ) { - //consensus: ["third"] + // consensus: ["third"] const string json = """ [ @@ -268,7 +267,7 @@ public void BracketNotationWithNumber( string query, Type sourceType ) "fifth" ] """; - var source = GetDocumentProxyFromSource( sourceType, json ); + var source = GetDocumentFromSource( sourceType, json ); var matches = source.Select( query ).ToList(); @@ -281,7 +280,7 @@ public void BracketNotationWithNumber( string query, Type sourceType ) [DataRow( "$[-1]", typeof( JsonNode ) )] public void BracketNotationWithNumberNegative1( string query, Type sourceType ) { - //consensus: ["third"] + // consensus: ["third"] const string json = """ [ @@ -290,7 +289,7 @@ public void BracketNotationWithNumberNegative1( string query, Type sourceType ) "third" ] """; - var source = GetDocumentProxyFromSource( sourceType, json ); + var source = GetDocumentFromSource( sourceType, json ); var matches = source.Select( query ).ToList(); @@ -303,13 +302,13 @@ public void BracketNotationWithNumberNegative1( string query, Type sourceType ) [DataRow( "$[-1]", typeof( JsonNode ) )] public void BracketNotationWithNumberNegative1OnEmptyArray( string query, Type sourceType ) { - //consensus: [] + // consensus: [] const string json = "[]"; - var source = GetDocumentProxyFromSource( sourceType, json ); + var source = GetDocumentFromSource( sourceType, json ); var matches = source.Select( query ); - var expected = source.ArrayEmpty; + var expected = Enumerable.Empty(); Assert.IsTrue( expected.SequenceEqual( matches ) ); } @@ -319,7 +318,7 @@ public void BracketNotationWithNumberNegative1OnEmptyArray( string query, Type s [DataRow( "$[0]", typeof( JsonNode ) )] public void BracketNotationWithNumber0( string query, Type sourceType ) { - //consensus: ["first"] + // consensus: ["first"] const string json = """ [ @@ -330,7 +329,7 @@ public void BracketNotationWithNumber0( string query, Type sourceType ) "fifth" ] """; - var source = GetDocumentProxyFromSource( sourceType, json ); + var source = GetDocumentFromSource( sourceType, json ); var matches = source.Select( query ).ToList(); @@ -343,7 +342,7 @@ public void BracketNotationWithNumber0( string query, Type sourceType ) [DataRow( "$.*[1]", typeof( JsonNode ) )] public void BracketNotationWithNumberAfterDotNotationWithWildcardOnNestedArraysWithDifferentLength( string query, Type sourceType ) { - //consensus: [3] + // consensus: [3] const string json = """ [ @@ -351,12 +350,12 @@ public void BracketNotationWithNumberAfterDotNotationWithWildcardOnNestedArraysW [2, 3] ] """; - var source = GetDocumentProxyFromSource( sourceType, json ); + var source = GetDocumentFromSource( sourceType, json ); var matches = source.Select( query ); var expected = new[] { - source.GetPropertyFromPath("$[1][1]") + source.FromJsonPathPointer("$[1][1]") }; Assert.IsTrue( expected.SequenceEqual( matches ) ); @@ -367,21 +366,17 @@ public void BracketNotationWithNumberAfterDotNotationWithWildcardOnNestedArraysW [DataRow( "$[0]", typeof( JsonNode ) )] public void BracketNotationWithNumberOnObject( string query, Type sourceType ) { - //consensus: [] - //deviation: ["value"] + // consensus: [] const string json = """ { "0": "value" } """; - var source = GetDocumentProxyFromSource( sourceType, json ); + var source = GetDocumentFromSource( sourceType, json ); var matches = source.Select( query ); - var expected = new[] - { - source.GetPropertyFromPath("$['0']") - }; + var expected = Enumerable.Empty(); Assert.IsTrue( expected.SequenceEqual( matches ) ); } @@ -391,17 +386,17 @@ public void BracketNotationWithNumberOnObject( string query, Type sourceType ) [DataRow( "$[1]", typeof( JsonNode ) )] public void BracketNotationWithNumberOnShortArray( string query, Type sourceType ) { - //consensus: [] + // consensus: [] const string json = """ [ "one element" ] """; - var source = GetDocumentProxyFromSource( sourceType, json ); + var source = GetDocumentFromSource( sourceType, json ); var matches = source.Select( query ); - var expected = source.ArrayEmpty; + var expected = Enumerable.Empty(); Assert.IsTrue( expected.SequenceEqual( matches ) ); } @@ -412,11 +407,11 @@ public void BracketNotationWithNumberOnShortArray( string query, Type sourceType [DataRow("$[0]", typeof(JsonNode))] public void BracketNotationWithNumberOnString(string query, Type sourceType) { - //consensus: [] - //deviation: NOT_SUPPORTED //JsonDocument can't parse + // rfc: NOT_SUPPORTED // JsonDocument can't parse + // consensus: [] const string json = "Hello World"; - var source = GetDocumentProxyFromSource(sourceType, json); + var source = GetDocumentFromSource(sourceType, json); } */ @@ -425,7 +420,7 @@ public void BracketNotationWithNumberOnString(string query, Type sourceType) [DataRow( "$[':']", typeof( JsonNode ) )] public void BracketNotationWithQuotedArraySliceLiteral( string query, Type sourceType ) { - //consensus: ["value"] + // consensus: ["value"] const string json = """ { @@ -433,7 +428,7 @@ public void BracketNotationWithQuotedArraySliceLiteral( string query, Type sourc "another": "entry" } """; - var source = GetDocumentProxyFromSource( sourceType, json ); + var source = GetDocumentFromSource( sourceType, json ); var matches = source.Select( query ).ToList(); @@ -446,14 +441,14 @@ public void BracketNotationWithQuotedArraySliceLiteral( string query, Type sourc [DataRow( "$[']']", typeof( JsonNode ) )] public void BracketNotationWithQuotedClosingBracketLiteral( string query, Type sourceType ) { - //consensus: [42] + // consensus: [42] const string json = """ { "]": 42 } """; - var source = GetDocumentProxyFromSource( sourceType, json ); + var source = GetDocumentFromSource( sourceType, json ); var matches = source.Select( query ).ToList(); @@ -466,7 +461,7 @@ public void BracketNotationWithQuotedClosingBracketLiteral( string query, Type s [DataRow( "$['@']", typeof( JsonNode ) )] public void BracketNotationWithQuotedCurrentObjectLiteral( string query, Type sourceType ) { - //consensus: ["value"] + // consensus: ["value"] const string json = """ { @@ -474,7 +469,7 @@ public void BracketNotationWithQuotedCurrentObjectLiteral( string query, Type so "another": "entry" } """; - var source = GetDocumentProxyFromSource( sourceType, json ); + var source = GetDocumentFromSource( sourceType, json ); var matches = source.Select( query ).ToList(); @@ -487,7 +482,7 @@ public void BracketNotationWithQuotedCurrentObjectLiteral( string query, Type so [DataRow( "$['.']", typeof( JsonNode ) )] public void BracketNotationWithQuotedDotLiteral( string query, Type sourceType ) { - //consensus: ["value"] + // consensus: ["value"] const string json = """ { @@ -495,12 +490,12 @@ public void BracketNotationWithQuotedDotLiteral( string query, Type sourceType ) "another": "entry" } """; - var source = GetDocumentProxyFromSource( sourceType, json ); + var source = GetDocumentFromSource( sourceType, json ); var matches = source.Select( query ); var expected = new[] { - source.GetPropertyFromPath("$['.']") + source.FromJsonPathPointer("$['.']") }; Assert.IsTrue( expected.SequenceEqual( matches ) ); @@ -511,7 +506,7 @@ public void BracketNotationWithQuotedDotLiteral( string query, Type sourceType ) [DataRow( "$['.*']", typeof( JsonNode ) )] public void BracketNotationWithQuotedDotWildcard( string query, Type sourceType ) { - //consensus: [1] + // consensus: [1] const string json = """ { @@ -520,12 +515,12 @@ public void BracketNotationWithQuotedDotWildcard( string query, Type sourceType "": 10 } """; - var source = GetDocumentProxyFromSource( sourceType, json ); + var source = GetDocumentFromSource( sourceType, json ); var matches = source.Select( query ); var expected = new[] { - source.GetPropertyFromPath("$['.*']") + source.FromJsonPathPointer("$['.*']") }; Assert.IsTrue( expected.SequenceEqual( matches ) ); @@ -537,11 +532,11 @@ public void BracketNotationWithQuotedDotWildcard( string query, Type sourceType [DataRow("$['\"']", typeof(JsonNode))] public void BracketNotationWithQuotedDoubleQuoteLiteral(string query, Type sourceType) { - //consensus: ["value"] - //deviation: NOT_SUPPORTED //JsonDocument can't parse + // rfc: NOT_SUPPORTED // JsonDocument can't parse + // consensus: ["value"] const string json = "{ \"\"\": \"value\", \"another\": \"entry\"}"; - var source = GetDocumentProxyFromSource(sourceType, json); + var source = GetDocumentFromSource(sourceType, json); } */ @@ -550,20 +545,20 @@ public void BracketNotationWithQuotedDoubleQuoteLiteral(string query, Type sourc [DataRow( @"$['\\']", typeof( JsonNode ) )] public void BracketNotationWithQuotedEscapedBackslash( string query, Type sourceType ) { - //consensus: //none - //deviation: ["value"] + // rfc: ["value"] + // consensus: none const string json = """ { "\\": "value" } """; - var source = GetDocumentProxyFromSource( sourceType, json ); + var source = GetDocumentFromSource( sourceType, json ); var matches = source.Select( query ); var expected = new[] { - source.GetPropertyFromPath(@"$['\']") + source.FromJsonPathPointer(@"$['\']") }; Assert.IsTrue( expected.SequenceEqual( matches ) ); @@ -574,15 +569,15 @@ public void BracketNotationWithQuotedEscapedBackslash( string query, Type source [DataRow( "$['\\'']", typeof( JsonNode ) )] public void BracketNotationWithQuotedEscapedSingleQuote( string query, Type sourceType ) { - //consensus: //none - //deviation: ["value"] + // rfc: ["value"] + // consensus: none const string json = """ { "'": "value" } """; - var source = GetDocumentProxyFromSource( sourceType, json ); + var source = GetDocumentFromSource( sourceType, json ); var matches = source.Select( query ).ToList(); @@ -595,19 +590,19 @@ public void BracketNotationWithQuotedEscapedSingleQuote( string query, Type sour [DataRow( "$['0']", typeof( JsonNode ) )] public void BracketNotationWithQuotedNumberOnObject( string query, Type sourceType ) { - //consensus: ["value"] + // consensus: ["value"] const string json = """ { "0": "value" } """; - var source = GetDocumentProxyFromSource( sourceType, json ); + var source = GetDocumentFromSource( sourceType, json ); var matches = source.Select( query ); var expected = new[] { - source.GetPropertyFromPath("$['0']") + source.FromJsonPathPointer("$['0']") }; Assert.IsTrue( expected.SequenceEqual( matches ) ); @@ -618,7 +613,7 @@ public void BracketNotationWithQuotedNumberOnObject( string query, Type sourceTy [DataRow( "$['$']", typeof( JsonNode ) )] public void BracketNotationWithQuotedRootLiteral( string query, Type sourceType ) { - //consensus: ["value"] + // consensus: ["value"] const string json = """ { @@ -626,31 +621,31 @@ public void BracketNotationWithQuotedRootLiteral( string query, Type sourceType "another": "entry" } """; - var source = GetDocumentProxyFromSource( sourceType, json ); + var source = GetDocumentFromSource( sourceType, json ); var matches = source.Select( query ); var expected = new[] { - source.GetPropertyFromPath("$['$']") + source.FromJsonPathPointer("$['$']") }; Assert.IsTrue( expected.SequenceEqual( matches ) ); } [DataTestMethod] - [DataRow( @"$[':@.\""$,*\'\\']", typeof( JsonDocument ) )] // $[':@.\"$,*'\\'] - [DataRow( @"$[':@.\""$,*\'\\']", typeof( JsonNode ) )] // $[':@.\"$,*'\\'] + [DataRow( """$[':@.\"$,*\'\\']""", typeof( JsonDocument ) )] + [DataRow( """$[':@.\"$,*\'\\']""", typeof( JsonNode ) )] public void BracketNotationWithQuotedSpecialCharactersCombined( string query, Type sourceType ) { - //consensus: //none - //deviation: 42 + // rfc: 42 + // consensus: none const string json = """ { ":@.\"$,*'\\": 42 } """; - var source = GetDocumentProxyFromSource( sourceType, json ); + var source = GetDocumentFromSource( sourceType, json ); var matches = source.Select( query ).ToList(); @@ -663,18 +658,18 @@ public void BracketNotationWithQuotedSpecialCharactersCombined( string query, Ty [DataRow( "$['single'quote']", typeof( JsonNode ) )] public void BracketNotationWithQuotedStringAndUnescapedSingleQuote( string query, Type sourceType ) { - //consensus: NOT_SUPPORTED + // consensus: NOT_SUPPORTED const string json = """ { "single'quote": "value" } """; - var source = GetDocumentProxyFromSource( sourceType, json ); + var source = GetDocumentFromSource( sourceType, json ); Assert.ThrowsException( () => { - var _ = source.Select( query ).ToList(); + _ = source.Select( query ).ToList(); } ); } @@ -683,7 +678,7 @@ public void BracketNotationWithQuotedStringAndUnescapedSingleQuote( string query [DataRow( "$[',']", typeof( JsonNode ) )] public void BracketNotationWithQuotedUnionLiteral( string query, Type sourceType ) { - //consensus: ["value"] + // consensus: ["value"] const string json = """ { @@ -691,12 +686,12 @@ public void BracketNotationWithQuotedUnionLiteral( string query, Type sourceType "another": "entry" } """; - var source = GetDocumentProxyFromSource( sourceType, json ); + var source = GetDocumentFromSource( sourceType, json ); var matches = source.Select( query ); var expected = new[] { - source.GetPropertyFromPath("$[',']") + source.FromJsonPathPointer("$[',']") }; Assert.IsTrue( expected.SequenceEqual( matches ) ); @@ -707,7 +702,7 @@ public void BracketNotationWithQuotedUnionLiteral( string query, Type sourceType [DataRow( "$['*']", typeof( JsonNode ) )] public void BracketNotationWithQuotedWildcardLiteral( string query, Type sourceType ) { - //consensus: ["value"] + // consensus: ["value"] const string json = """ { @@ -715,12 +710,12 @@ public void BracketNotationWithQuotedWildcardLiteral( string query, Type sourceT "another": "entry" } """; - var source = GetDocumentProxyFromSource( sourceType, json ); + var source = GetDocumentFromSource( sourceType, json ); var matches = source.Select( query ); var expected = new[] { - source.GetPropertyFromPath("$['*']") + source.FromJsonPathPointer("$['*']") }; Assert.IsTrue( expected.SequenceEqual( matches ) ); @@ -731,17 +726,17 @@ public void BracketNotationWithQuotedWildcardLiteral( string query, Type sourceT [DataRow( "$['*']", typeof( JsonNode ) )] public void BracketNotationWithQuotedWildcardLiteralOnObjectWithoutKey( string query, Type sourceType ) { - //consensus: [] + // consensus: [] const string json = """ { "another": "entry" } """; - var source = GetDocumentProxyFromSource( sourceType, json ); + var source = GetDocumentFromSource( sourceType, json ); var matches = source.Select( query ); - var expected = source.ArrayEmpty; + var expected = Enumerable.Empty(); Assert.IsTrue( expected.SequenceEqual( matches ) ); } @@ -751,7 +746,7 @@ public void BracketNotationWithQuotedWildcardLiteralOnObjectWithoutKey( string q [DataRow( "$[ 'a' ]", typeof( JsonNode ) )] public void BracketNotationWithSpaces( string query, Type sourceType ) { - //consensus: [2] + // consensus: [2] const string json = """ { @@ -766,12 +761,12 @@ public void BracketNotationWithSpaces( string query, Type sourceType ) "\"a\"": 9 } """; - var source = GetDocumentProxyFromSource( sourceType, json ); + var source = GetDocumentFromSource( sourceType, json ); var matches = source.Select( query ); var expected = new[] { - source.GetPropertyFromPath("$['a']") + source.FromJsonPathPointer("$['a']") }; Assert.IsTrue( expected.SequenceEqual( matches ) ); @@ -782,7 +777,7 @@ public void BracketNotationWithSpaces( string query, Type sourceType ) [DataRow( "$['ni.*']", typeof( JsonNode ) )] public void BracketNotationWithStringIncludingDotWildcard( string query, Type sourceType ) { - //consensus: [1] + // consensus: [1] const string json = """ { @@ -791,12 +786,12 @@ public void BracketNotationWithStringIncludingDotWildcard( string query, Type so "mice": 100 } """; - var source = GetDocumentProxyFromSource( sourceType, json ); + var source = GetDocumentFromSource( sourceType, json ); var matches = source.Select( query ); var expected = new[] { - source.GetPropertyFromPath("$['ni.*']") + source.FromJsonPathPointer("$['ni.*']") }; Assert.IsTrue( expected.SequenceEqual( matches ) ); @@ -807,7 +802,7 @@ public void BracketNotationWithStringIncludingDotWildcard( string query, Type so [DataRow( "$['two'.'some']", typeof( JsonNode ) )] public void BracketNotationWithTwoLiteralsSeparatedByDot( string query, Type sourceType ) { - //consensus: NOT_SUPPORTED + // consensus: NOT_SUPPORTED const string json = """ { @@ -822,11 +817,11 @@ public void BracketNotationWithTwoLiteralsSeparatedByDot( string query, Type sou "two'.'some": "43" } """; - var source = GetDocumentProxyFromSource( sourceType, json ); + var source = GetDocumentFromSource( sourceType, json ); Assert.ThrowsException( () => { - var _ = source.Select( query ).ToList(); + _ = source.Select( query ).ToList(); } ); } @@ -835,7 +830,7 @@ public void BracketNotationWithTwoLiteralsSeparatedByDot( string query, Type sou [DataRow( "$[two.some]", typeof( JsonNode ) )] public void BracketNotationWithTwoLiteralsSeparatedByDotWithoutQuotes( string query, Type sourceType ) { - //consensus: NOT_SUPPORTED + // consensus: NOT_SUPPORTED const string json = """ { @@ -849,11 +844,11 @@ public void BracketNotationWithTwoLiteralsSeparatedByDotWithoutQuotes( string qu "two.some": "42" } """; - var source = GetDocumentProxyFromSource( sourceType, json ); + var source = GetDocumentFromSource( sourceType, json ); Assert.ThrowsException( () => { - var _ = source.Select( query ).ToList(); + _ = source.Select( query ).ToList(); } ); } @@ -862,7 +857,7 @@ public void BracketNotationWithTwoLiteralsSeparatedByDotWithoutQuotes( string qu [DataRow( "$[0:2][*]", typeof( JsonNode ) )] public void BracketNotationWithWildcardAfterArraySlice( string query, Type sourceType ) { - //consensus: [1, 2, "a", "b"] + // consensus: [1, 2, "a", "b"] const string json = """ [ @@ -871,15 +866,15 @@ public void BracketNotationWithWildcardAfterArraySlice( string query, Type sourc [0, 0] ] """; - var source = GetDocumentProxyFromSource( sourceType, json ); + var source = GetDocumentFromSource( sourceType, json ); var matches = source.Select( query ).ToList(); var expected = new[] { - source.GetPropertyFromPath("$[0][0]"), - source.GetPropertyFromPath("$[0][1]"), - source.GetPropertyFromPath("$[1][0]"), - source.GetPropertyFromPath("$[1][1]") + source.FromJsonPathPointer("$[0][0]"), + source.FromJsonPathPointer("$[0][1]"), + source.FromJsonPathPointer("$[1][0]"), + source.FromJsonPathPointer("$[1][1]") }; Assert.IsTrue( expected.SequenceEqual( matches ) ); @@ -894,7 +889,7 @@ public void BracketNotationWithWildcardAfterArraySlice( string query, Type sourc [DataRow( "$[*].bar[*]", typeof( JsonNode ) )] public void BracketNotationWithWildcardAfterDotNotationAfterBracketNotationWithWildcard( string query, Type sourceType ) { - //consensus: [42] + // consensus: [42] const string json = """ [ @@ -903,12 +898,12 @@ public void BracketNotationWithWildcardAfterDotNotationAfterBracketNotationWithW } ] """; - var source = GetDocumentProxyFromSource( sourceType, json ); + var source = GetDocumentFromSource( sourceType, json ); var matches = source.Select( query ); var expected = new[] { - source.GetPropertyFromPath("$[0]['bar'][0]") + source.FromJsonPathPointer("$[0]['bar'][0]") }; Assert.IsTrue( expected.SequenceEqual( matches ) ); @@ -919,8 +914,8 @@ public void BracketNotationWithWildcardAfterDotNotationAfterBracketNotationWithW [DataRow( "$..[*]", typeof( JsonNode ) )] public void BracketNotationWithWildcardAfterRecursiveDescent( string query, Type sourceType ) { - //consensus: ["string", "value", 0, 1, [0, 1], {"complex": "string", "primitives": [0, 1]}] - //deviation: consensus results/different order //rfc in selector order + // rfc: in selector order + // consensus: ["string", "value", 0, 1, [0, 1], {"complex": "string", "primitives": [0, 1]}] const string json = """ { @@ -931,24 +926,22 @@ public void BracketNotationWithWildcardAfterRecursiveDescent( string query, Type } } """; - var source = GetDocumentProxyFromSource( sourceType, json ); + var source = GetDocumentFromSource( sourceType, json ); var matches = source.Select( query ).ToList(); var expected = new[] { - source.GetPropertyFromPath("$['key']"), - source.GetPropertyFromPath("$['another key']"), - source.GetPropertyFromPath("$['another key']['complex']"), - source.GetPropertyFromPath("$['another key']['primitives']"), - source.GetPropertyFromPath("$['another key']['primitives'][0]"), - source.GetPropertyFromPath("$['another key']['primitives'][1]") + source.FromJsonPathPointer("$['key']"), + source.FromJsonPathPointer("$['another key']"), + source.FromJsonPathPointer("$['another key']['complex']"), + source.FromJsonPathPointer("$['another key']['primitives']"), + source.FromJsonPathPointer("$['another key']['primitives'][0]"), + source.FromJsonPathPointer("$['another key']['primitives'][1]") }; - - Assert.IsTrue( expected.SequenceEqual( matches ) ); Assert.IsTrue( JsonValueHelper.GetString( matches[0] ) == "value" ); - Assert.IsTrue( JsonValueHelper.GetString( matches[1], minify: true ) == JsonValueHelper.MinifyJsonString( "{\"complex\": \"string\", \"primitives\": [0,1]}" ) ); + Assert.IsTrue( JsonValueHelper.GetString( matches[1], minify: true ) == JsonValueHelper.MinifyJson( """{"complex": "string", "primitives": [0,1]}""" ) ); Assert.IsTrue( JsonValueHelper.GetString( matches[2] ) == "string" ); Assert.IsTrue( JsonValueHelper.GetString( matches[3], minify: true ) == "[0,1]" ); Assert.IsTrue( JsonValueHelper.GetInt32( matches[4] ) == 0 ); @@ -960,7 +953,7 @@ public void BracketNotationWithWildcardAfterRecursiveDescent( string query, Type [DataRow( "$[*]", typeof( JsonNode ) )] public void BracketNotationWithWildcardOnArray( string query, Type sourceType ) { - //consensus: ["string", 42, {"key": "value"}, [0, 1]] + // consensus: ["string", 42, {"key": "value"}, [0, 1]] const string json = """ [ @@ -972,15 +965,15 @@ public void BracketNotationWithWildcardOnArray( string query, Type sourceType ) [0, 1] ] """; - var source = GetDocumentProxyFromSource( sourceType, json ); + var source = GetDocumentFromSource( sourceType, json ); var matches = source.Select( query ); var expected = new[] { - source.GetPropertyFromPath("$[0]"), - source.GetPropertyFromPath("$[1]"), - source.GetPropertyFromPath("$[2]"), - source.GetPropertyFromPath("$[3]") + source.FromJsonPathPointer("$[0]"), + source.FromJsonPathPointer("$[1]"), + source.FromJsonPathPointer("$[2]"), + source.FromJsonPathPointer("$[3]") }; Assert.IsTrue( expected.SequenceEqual( matches ) ); @@ -991,15 +984,13 @@ public void BracketNotationWithWildcardOnArray( string query, Type sourceType ) [DataRow( "$[*]", typeof( JsonNode ) )] public void BracketNotationWithWildcardOnEmptyArray( string query, Type sourceType ) { - //consensus: [] + // consensus: [] - const string json = """ - [] - """; - var source = GetDocumentProxyFromSource( sourceType, json ); + const string json = "[]"; + var source = GetDocumentFromSource( sourceType, json ); var matches = source.Select( query ); - var expected = source.ArrayEmpty; + var expected = Enumerable.Empty(); Assert.IsTrue( expected.SequenceEqual( matches ) ); } @@ -1009,15 +1000,13 @@ public void BracketNotationWithWildcardOnEmptyArray( string query, Type sourceTy [DataRow( "$[*]", typeof( JsonNode ) )] public void BracketNotationWithWildcardOnEmptyObject( string query, Type sourceType ) { - //consensus: [] + // consensus: [] - const string json = """ - {} - """; - var source = GetDocumentProxyFromSource( sourceType, json ); + const string json = "{}"; + var source = GetDocumentFromSource( sourceType, json ); var matches = source.Select( query ); - var expected = source.ArrayEmpty; + var expected = Enumerable.Empty(); Assert.IsTrue( expected.SequenceEqual( matches ) ); } @@ -1027,7 +1016,7 @@ public void BracketNotationWithWildcardOnEmptyObject( string query, Type sourceT [DataRow( "$[*]", typeof( JsonNode ) )] public void BracketNotationWithWildcardOnNullValueArray( string query, Type sourceType ) { - //consensus: [40, null, 42] + // consensus: [40, null, 42] const string json = """ [ @@ -1036,14 +1025,14 @@ public void BracketNotationWithWildcardOnNullValueArray( string query, Type sour 42 ] """; - var source = GetDocumentProxyFromSource( sourceType, json ); + var source = GetDocumentFromSource( sourceType, json ); var matches = source.Select( query ); var expected = new[] { - source.GetPropertyFromPath("$[0]"), - source.GetPropertyFromPath("$[1]"), - source.GetPropertyFromPath("$[2]") + source.FromJsonPathPointer("$[0]"), + source.FromJsonPathPointer("$[1]"), + source.FromJsonPathPointer("$[2]") }; Assert.IsTrue( expected.SequenceEqual( matches ) ); @@ -1054,8 +1043,8 @@ public void BracketNotationWithWildcardOnNullValueArray( string query, Type sour [DataRow( "$[*]", typeof( JsonNode ) )] public void BracketNotationWithWildcardOnObject( string query, Type sourceType ) { - //consensus: ["string", 42, [0, 1], {"key": "value"}] - //deviation: consensus results/different order //rfc in selector order + // rfc: in selector order + // consensus: ["string", 42, [0, 1], {"key": "value"}] const string json = """ { @@ -1067,15 +1056,15 @@ public void BracketNotationWithWildcardOnObject( string query, Type sourceType ) "array": [0, 1] } """; - var source = GetDocumentProxyFromSource( sourceType, json ); + var source = GetDocumentFromSource( sourceType, json ); var matches = source.Select( query ); var expected = new[] { - source.GetPropertyFromPath("$['some']"), - source.GetPropertyFromPath("$['int']"), - source.GetPropertyFromPath("$['object']"), - source.GetPropertyFromPath("$['array']") + source.FromJsonPathPointer("$['some']"), + source.FromJsonPathPointer("$['int']"), + source.FromJsonPathPointer("$['object']"), + source.FromJsonPathPointer("$['array']") }; Assert.IsTrue( expected.SequenceEqual( matches ) ); @@ -1086,18 +1075,18 @@ public void BracketNotationWithWildcardOnObject( string query, Type sourceType ) [DataRow( "$[key]", typeof( JsonNode ) )] public void BracketNotationWithoutQuotes( string query, Type sourceType ) { - //consensus: NOT_SUPPORTED + // consensus: NOT_SUPPORTED const string json = """ { "key": "value" } """; - var source = GetDocumentProxyFromSource( sourceType, json ); + var source = GetDocumentFromSource( sourceType, json ); Assert.ThrowsException( () => { - var _ = source.Select( query ).ToList(); + _ = source.Select( query ).ToList(); } ); } } diff --git a/test/Hyperbee.Json.Tests/Query/JsonPathDotNotationTests.cs b/test/Hyperbee.Json.Tests/Query/JsonPathDotNotationTests.cs index d699260f..6793e011 100644 --- a/test/Hyperbee.Json.Tests/Query/JsonPathDotNotationTests.cs +++ b/test/Hyperbee.Json.Tests/Query/JsonPathDotNotationTests.cs @@ -27,7 +27,7 @@ public void DotBracketNotationWithoutQuotes( string query, Type sourceType ) } } """; - var source = GetDocumentProxyFromSource( sourceType, json ); + var source = GetDocumentFromSource( sourceType, json ); Assert.ThrowsException( () => { @@ -47,7 +47,7 @@ public void DotBracketNotationWithEmptyPath( string query, Type sourceType ) "''": "nice" } """; - var source = GetDocumentProxyFromSource( sourceType, json ); + var source = GetDocumentFromSource( sourceType, json ); Assert.ThrowsException( () => { @@ -60,21 +60,21 @@ public void DotBracketNotationWithEmptyPath( string query, Type sourceType ) [DataRow( "$.屬性", typeof( JsonNode ) )] public void DotNotationWithNonAsciiKey( string query, Type sourceType ) { + // consensus: none + const string json = """ { "\u5c6c\u6027": "value" } """; - var source = GetDocumentProxyFromSource( sourceType, json ); + var source = GetDocumentFromSource( sourceType, json ); var matches = source.Select( query ).ToList(); var expected = new[] { - source.GetPropertyFromPath("$['屬性']") + source.FromJsonPathPointer("$['屬性']") }; - // no consensus - Assert.IsTrue( expected.SequenceEqual( matches ) ); Assert.IsTrue( JsonValueHelper.GetString( matches[0] ) == "value" ); } @@ -90,7 +90,7 @@ public void DotNotationWithoutDot( string query, Type sourceType ) "$a": 2 } """; - var source = GetDocumentProxyFromSource( sourceType, json ); + var source = GetDocumentFromSource( sourceType, json ); Assert.ThrowsException( () => { diff --git a/test/Hyperbee.Json.Tests/Query/JsonPathFilterExpressionTests.cs b/test/Hyperbee.Json.Tests/Query/JsonPathFilterExpressionTests.cs new file mode 100644 index 00000000..1ee3663a --- /dev/null +++ b/test/Hyperbee.Json.Tests/Query/JsonPathFilterExpressionTests.cs @@ -0,0 +1,632 @@ +using System; +using System.Linq; +using System.Text.Json; +using System.Text.Json.Nodes; +using Hyperbee.Json.Tests.TestSupport; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Hyperbee.Json.Tests.Query; + +[TestClass] +public class JsonPathFilterExpressionTests : JsonTestBase +{ + [DataTestMethod] + [DataRow( "$[?(@.key)]", typeof( JsonDocument ) )] + [DataRow( "$[?(@.key)]", typeof( JsonNode ) )] + [DataRow( "$[? @.key]", typeof( JsonDocument ) )] + [DataRow( "$[? @.key]", typeof( JsonNode ) )] + public void FilterExpressionWithArrayTruthyProperty( string query, Type sourceType ) + { + const string json = + """ + [ + {"some": "some value"}, + {"key": "value"} + ] + """; + var source = GetDocumentFromSource( sourceType, json ); + + var matches = source.Select( query ); + var expected = new[] { source.FromJsonPathPointer( "$[1]" ) }; + + Assert.IsTrue( expected.SequenceEqual( matches ) ); + } + + [DataTestMethod] + [DataRow( "$[?(@.key)]", typeof( JsonDocument ) )] + [DataRow( "$[?(@.key)]", typeof( JsonNode ) )] + public void FilterExpressionWithTruthyProperty( string query, Type sourceType ) + { + const string json = + """ + { + "key": 42, + "another": { + "key": 1 + } + } + """; + var source = GetDocumentFromSource( sourceType, json ); + + var matches = source.Select( query ); + var expected = new[] { source.FromJsonPathPointer( "$['another']" ) }; + + Assert.IsTrue( expected.SequenceEqual( matches ) ); + } + + [DataTestMethod] + [DataRow( "$[?(@.key<42)]", typeof( JsonDocument ) )] + [DataRow( "$[?(@.key<42)]", typeof( JsonNode ) )] + [DataRow( "$[?@.key < 42]", typeof( JsonDocument ) )] + [DataRow( "$[?@.key < 42]", typeof( JsonNode ) )] + public void FilterExpressionWithLessThan( string query, Type sourceType ) + { + const string json = + """ + [ + {"key": 0}, + {"key": 42}, + {"key": -1}, + {"key": 41}, + {"key": 43}, + {"key": 42.0001}, + {"key": 41.9999}, + {"key": 100}, + {"some": "value"} + ] + """; + + var source = GetDocumentFromSource( sourceType, json ); + + var matches = source.Select( query ); + var expected = new[] { source.FromJsonPathPointer( "$[0]" ), source.FromJsonPathPointer( "$[2]" ), source.FromJsonPathPointer( "$[3]" ), source.FromJsonPathPointer( "$[6]" ) }; + + 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 ) )] + public void FilterExpressionAfterDoNotationWithWildcardAfterRecursiveDecent( string query, Type sourceType ) + { + // consensus: [{"id": 3, "name": "another"}, {"id": 4, "name": "more"}, {"id": 5, "name": "next to last"}] + + const string json = + """ + [ + { + "complex": { + "one": [ + { + "name": "first", + "id": 1 + }, + { + "name": "next", + "id": 2 + }, + { + "name": "another", + "id": 3 + }, + { + "name": "more", + "id": 4 + } + ], + "more": { + "name": "next to last", + "id": 5 + } + } + }, + { + "name": "last", + "id": 6 + } + ] + """; + + var source = GetDocumentFromSource( sourceType, json ); + + var matches = source.Select( query ); + var expected = new[] + { + source.FromJsonPathPointer( "$[0].complex.more" ), + source.FromJsonPathPointer( "$[0].complex.one[2]" ), + source.FromJsonPathPointer( "$[0].complex.one[3]" ) + }; + + Assert.IsTrue( expected.SequenceEqual( matches ) ); + } + + [DataTestMethod] + [DataRow( "$[?(@.a && (@.b || @.c))]", typeof( JsonDocument ) )] + [DataRow( "$[?(@.a && (@.b || @.c))]", typeof( JsonNode ) )] + public void FilterExpressionWithDifferentGroupedOperators( string query, Type sourceType ) + { + const string json = + """ + [ + { + "a": true + }, + { + "a": true, + "b": true + }, + { + "a": true, + "b": true, + "c": true + }, + { + "b": true, + "c": true + }, + { + "a": true, + "c": true + }, + { + "c": true + }, + { + "b": true + } + ] + """; + + var source = GetDocumentFromSource( sourceType, json ); + + var matches = source.Select( query ); + var expected = new[] { source.FromJsonPathPointer( "$[1]" ), source.FromJsonPathPointer( "$[2]" ), source.FromJsonPathPointer( "$[4]" ) }; + + Assert.IsTrue( expected.SequenceEqual( matches ) ); + } + + + [DataTestMethod] + [DataRow( "$[?(@.a && @.b || @.c)]", typeof( JsonDocument ) )] + [DataRow( "$[?(@.a && @.b || @.c)]", typeof( JsonNode ) )] + public void FilterExpressionWithDifferentUngroupedOperators( string query, Type sourceType ) + { + const string json = + """ + [ + { + "a": true, + "b": true + }, + { + "a": true, + "b": true, + "c": true + }, + { + "b": true, + "c": true + }, + { + "a": true, + "c": true + }, + { + "a": true + }, + { + "b": true + }, + { + "c": true + }, + { + "d": true + }, + {} + ] + + """; + + var source = GetDocumentFromSource( sourceType, json ); + + var matches = source.Select( query ); + var expected = new[] { source.FromJsonPathPointer( "$[0]" ), source.FromJsonPathPointer( "$[1]" ), source.FromJsonPathPointer( "$[2]" ), source.FromJsonPathPointer( "$[3]" ), source.FromJsonPathPointer( "$[6]" ) }; + + Assert.IsTrue( expected.SequenceEqual( matches ) ); + } + + [DataTestMethod] + [DataRow( "$[?(@.d == [\"v1\", \"v2\"])]", typeof( JsonDocument ) )] + [DataRow( "$[?(@.d == [\"v1\", \"v2\"])]", typeof( JsonNode ) )] + public void FilterExpressionWithEqualsArray( string query, Type sourceType ) + { + const string json = + """ + [ + { + "d": [ + "v1", + "v2" + ] + }, + { + "d": [ + "a", + "b" + ] + }, + { + "d": "v1" + }, + { + "d": "v2" + }, + { + "d": {} + }, + { + "d": [] + }, + { + "d": null + }, + { + "d": -1 + }, + { + "d": 0 + }, + { + "d": 1 + }, + { + "d": "['v1','v2']" + }, + { + "d": "['v1', 'v2']" + }, + { + "d": "v1,v2" + }, + { + "d": "[\"v1\", \"v2\"]" + }, + { + "d": "[\"v1\",\"v2\"]" + } + ] + """; + + var source = GetDocumentFromSource( sourceType, json ); + + var matches = source.Select( query ); + var expected = new[] { source.FromJsonPathPointer( "$[0]" ) }; + + Assert.IsTrue( expected.SequenceEqual( matches ) ); + } + + [DataTestMethod] + [DataRow( "$[?(@[0:1]==[1])]", typeof( JsonDocument ) )] + [DataRow( "$[?(@[0:1]==[1])]", typeof( JsonNode ) )] + public void FilterExpressionWithEqualsArrayForSliceWithRange1( string query, Type sourceType ) + { + // consensus: NOT_SUPPORTED + // deviation: [] ??? should return [1]? + + var json = + """ + [ + [1, 2, 3], + [1], + [2, 3], + 1, + 2 + ] + """; + + 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 ) ); + } + + [DataTestMethod] + [DataRow( "$[?(@.*==[1,2])]", typeof( JsonDocument ) )] + [DataRow( "$[?(@.*==[1,2])]", typeof( JsonNode ) )] + public void FilterExpressionWithEqualsArrayForDotNotationWithStart( string query, Type sourceType ) + { + // consensus: NOT_SUPPORTED + // deviation: [] + + var json = + """ + [ + [1,2], + [2,3], + [1], + [2], + [1, 2, 3], + 1, + 2, + 3 + ] + """; + + var source = GetDocumentFromSource( sourceType, json ); + + var matches = source.Select( query ); + var expected = Enumerable.Empty(); + + Assert.IsTrue( expected.SequenceEqual( matches ) ); + } + + [DataTestMethod] + [DataRow( "$[?(@.d==[\"v1\",\"v2\"] || (@.d == true))]", typeof( JsonDocument ) )] + [DataRow( "$[?(@.d==[\"v1\",\"v2\"] || (@.d == true))]", typeof( JsonNode ) )] + public void FilterExpressionWithEqualsArrayOrEqualsTrue( string query, Type sourceType ) + { + // consensus: NOT_SUPPORTED + // deviation: [{"d":["v1","v2"]},{"d":true}] + + var json = + """ + [ + {"d": ["v1", "v2"] }, + {"d": ["a", "b"] }, + {"d" : true} + ] + """; + + var source = GetDocumentFromSource( sourceType, json ); + + var matches = source.Select( query ); + var expected = new[] { source.FromJsonPathPointer( "$[0]" ), source.FromJsonPathPointer( "$[2]" ) }; + + Assert.IsTrue( expected.SequenceEqual( matches ) ); + } + + [DataTestMethod] + [DataRow( "$[?(@.d==['v1','v2'])]", typeof( JsonDocument ) )] + [DataRow( "$[?(@.d==['v1','v2'])]", typeof( JsonNode ) )] + [ExpectedException( typeof( NotSupportedException ) )] + public void FilterExpressionWithEqualsArrayWithSingleQuotes( string query, Type sourceType ) + { + // consensus: NOT_SUPPORTED + + var json = + """ + [ + { + "d": [ + "v1", + "v2" + ] + }, + { + "d": [ + "a", + "b" + ] + }, + { + "d": "v1" + }, + { + "d": "v2" + }, + { + "d": {} + }, + { + "d": [] + }, + { + "d": null + }, + { + "d": -1 + }, + { + "d": 0 + }, + { + "d": 1 + }, + { + "d": "['v1','v2']" + }, + { + "d": "['v1', 'v2']" + }, + { + "d": "v1,v2" + }, + { + "d": "[\"v1\", \"v2\"]" + }, + { + "d": "[\"v1\",\"v2\"]" + } + ] + + """; + + var source = GetDocumentFromSource( sourceType, json ); + + _ = source.Select( query ).ToArray(); + } + + [DataTestMethod] + [DataRow( "$[?((@.key<44)==false)]", typeof( JsonDocument ) )] + [DataRow( "$[?((@.key<44)==false)]", typeof( JsonNode ) )] + public void FilterExpressionWithEqualsBooleanExpressionValue( string query, Type sourceType ) + { + // consensus: NOT_SUPPORTED + // deviation: [{"key":44}] as per rfc + + var json = + """ + [ + {"key": 42}, + {"key": 43}, + {"key": 44} + ] + """; + + var source = GetDocumentFromSource( sourceType, json ); + + var matches = source.Select( query ); + var expected = new[] { source.FromJsonPathPointer( "$[2]" ) }; + + Assert.IsTrue( expected.SequenceEqual( matches ) ); + } + + [DataTestMethod] + [DataRow( "$[?(@.key==false)]", typeof( JsonDocument ) )] + [DataRow( "$[?(@.key==false)]", typeof( JsonNode ) )] + public void FilterExpressionWithEqualsFalse( string query, Type sourceType ) + { + // consensus: [{"key": false}] + + var json = + """ + [ + { + "some": "some value" + }, + { + "key": true + }, + { + "key": false + }, + { + "key": null + }, + { + "key": "value" + }, + { + "key": "" + }, + { + "key": 0 + }, + { + "key": 1 + }, + { + "key": -1 + }, + { + "key": 42 + }, + { + "key": {} + }, + { + "key": [] + } + ] + + """; + + var source = GetDocumentFromSource( sourceType, json ); + + var matches = source.Select( query ); + var expected = new[] { source.FromJsonPathPointer( "$[2]" ) }; + + Assert.IsTrue( expected.SequenceEqual( matches ) ); + } + + [DataTestMethod] + [DataRow( "$[?(@.key==null)]", typeof( JsonDocument ) )] + [DataRow( "$[?(@.key==null)]", typeof( JsonNode ) )] + public void FilterExpressionWithEqualsNull( string query, Type sourceType ) + { + // consensus: [{"key": null}] + + var json = + """ + [ + { + "some": "some value" + }, + { + "key": true + }, + { + "key": false + }, + { + "key": null + }, + { + "key": "value" + }, + { + "key": "" + }, + { + "key": 0 + }, + { + "key": 1 + }, + { + "key": -1 + }, + { + "key": 42 + }, + { + "key": {} + }, + { + "key": [] + } + ] + """; + + var source = GetDocumentFromSource( sourceType, json ); + + var matches = source.Select( query ); + var expected = new[] { source.FromJsonPathPointer( "$[3]" ) }; + + Assert.IsTrue( expected.SequenceEqual( matches ) ); + } +} + diff --git a/test/Hyperbee.Json.Tests/Query/JsonPathFilterTests.cs b/test/Hyperbee.Json.Tests/Query/JsonPathFilterTests.cs deleted file mode 100644 index 07fdfc6e..00000000 --- a/test/Hyperbee.Json.Tests/Query/JsonPathFilterTests.cs +++ /dev/null @@ -1,73 +0,0 @@ -using System; -using System.Linq; -using System.Text.Json; -using System.Text.Json.Nodes; -using Hyperbee.Json.Tests.TestSupport; -using Microsoft.VisualStudio.TestTools.UnitTesting; - -namespace Hyperbee.Json.Tests.Query; - -[TestClass] -public class JsonPathFilterTests : JsonTestBase -{ - [DataTestMethod] - [DataRow( "$[?(@.key)]", typeof( JsonDocument ) )] - [DataRow( "$[?(@.key)]", typeof( JsonNode ) )] - [DataRow( "$[? @.key]", typeof( JsonDocument ) )] - [DataRow( "$[? @.key]", typeof( JsonNode ) )] - public void FilterWithTruthyProperty( string query, Type sourceType ) - { - const string json = - """ - [ - {"some": "some value"}, - {"key": "value"} - ] - """; - var source = GetDocumentProxyFromSource( sourceType, json ); - - var matches = source.Select( query ); - var expected = new[] - { - source.GetPropertyFromPath( "$[1]" ) - }; - - Assert.IsTrue( expected.SequenceEqual( matches ) ); - } - - [DataTestMethod] - [DataRow( "$[?(@.key<42)]", typeof( JsonDocument ) )] - [DataRow( "$[?(@.key<42)]", typeof( JsonNode ) )] - [DataRow( "$[?@.key < 42]", typeof( JsonDocument ) )] - [DataRow( "$[?@.key < 42]", typeof( JsonNode ) )] - public void FilterWithLessThan( string query, Type sourceType ) - { - const string json = - """ - [ - {"key": 0}, - {"key": 42}, - {"key": -1}, - {"key": 41}, - {"key": 43}, - {"key": 42.0001}, - {"key": 41.9999}, - {"key": 100}, - {"some": "value"} - ] - """; - - var source = GetDocumentProxyFromSource( sourceType, json ); - - var matches = source.Select( query ); - var expected = new[] - { - source.GetPropertyFromPath( "$[0]" ), - source.GetPropertyFromPath( "$[2]" ), - source.GetPropertyFromPath( "$[3]" ), - source.GetPropertyFromPath( "$[6]" ) - }; - - Assert.IsTrue( expected.SequenceEqual( matches ) ); - } -} diff --git a/test/Hyperbee.Json.Tests/Query/JsonPathRecursiveDescentTests.cs b/test/Hyperbee.Json.Tests/Query/JsonPathRecursiveDescentTests.cs new file mode 100644 index 00000000..94768b71 --- /dev/null +++ b/test/Hyperbee.Json.Tests/Query/JsonPathRecursiveDescentTests.cs @@ -0,0 +1,142 @@ +using System; +using System.Linq; +using System.Text.Json; +using System.Text.Json.Nodes; +using Hyperbee.Json.Tests.TestSupport; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Hyperbee.Json.Tests.Query; + +[TestClass] +public class JsonPathRecursiveDescentTests : JsonTestBase +{ + [DataTestMethod] + [DataRow( "$..", typeof( JsonDocument ) )] + [DataRow( "$..", typeof( JsonNode ) )] + [ExpectedException( typeof( NotSupportedException ) )] + public void RecursiveDescent( string query, Type sourceType ) + { + // consensus: none + + const string json = """ + [ + {"a": {"b": "c"}}, + [0, 1] + ] + """; + + var source = GetDocumentFromSource( sourceType, json ); + + _ = source.Select( query ).ToList(); + } + + [DataTestMethod] + [DataRow( "$..*", typeof( JsonDocument ) )] + [DataRow( "$..*", typeof( JsonNode ) )] + public void RecursiveDescentOnNestedArrays( string query, Type sourceType ) + { + const string json = """ + [ + [0], + [1] + ] + """; + + var source = GetDocumentFromSource( sourceType, json ); + + var matches = source.Select( query ).ToList(); + var expected = new[] + { + source.FromJsonPathPointer( "$[0]" ), + source.FromJsonPathPointer( "$[1]" ), + source.FromJsonPathPointer( "$[0][0]" ), + source.FromJsonPathPointer( "$[1][0]" ) + }; + + Assert.IsTrue( expected.SequenceEqual( matches ) ); + } + + [DataTestMethod] + [DataRow( "$.key..", typeof( JsonDocument ) )] + [DataRow( "$.key..", typeof( JsonNode ) )] + [ExpectedException( typeof( NotSupportedException ) )] + public void RecursiveDescentAfterDotNotation( string query, Type sourceType ) + { + // consensus: NOT_SUPPORTED + + const string json = """ + { + "some key": "value", + "key": { + "complex": "string", + "primitives": [0, 1] + } + } + """; + + var source = GetDocumentFromSource( sourceType, json ); + + _ = source.Select( query ).ToList(); + } + + [DataTestMethod] + [DataRow( "$..[1].key", typeof( JsonDocument ) )] + [DataRow( "$..[1].key", typeof( JsonNode ) )] + public void DotNotationAfterBracketNotationAfterRecursiveDescent( string query, Type sourceType ) + { + // consensus: [200, 42, 500] + + const string json = """ + { + "k": [ + { + "key": "some value" + }, + { + "key": 42 + } + ], + "kk": [ + [ + { + "key": 100 + }, + { + "key": 200 + }, + { + "key": 300 + } + ], + [ + { + "key": 400 + }, + { + "key": 500 + }, + { + "key": 600 + } + ] + ], + "key": [ + 0, + 1 + ] + } + """; + + var source = GetDocumentFromSource( sourceType, json ); + + var matches = source.Select( query ).ToList(); + var expected = new[] + { + source.FromJsonPathPointer( "$.k[1].key" ), + source.FromJsonPathPointer( "$.kk[0][1].key" ), + source.FromJsonPathPointer( "$.kk[1][1].key" ), + }; + + Assert.IsTrue( expected.SequenceEqual( matches ) ); + } +} diff --git a/test/Hyperbee.Json.Tests/Query/JsonPathRootOnScalarTests.cs b/test/Hyperbee.Json.Tests/Query/JsonPathRootOnScalarTests.cs index 8c8f03fe..f93998a7 100644 --- a/test/Hyperbee.Json.Tests/Query/JsonPathRootOnScalarTests.cs +++ b/test/Hyperbee.Json.Tests/Query/JsonPathRootOnScalarTests.cs @@ -15,17 +15,17 @@ public class JsonPathRootOnScalarTests : JsonTestBase [DataRow( "$", typeof( JsonNode ) )] public void RootOnScalar( string query, Type sourceType ) { + // consensus: none + const string json = "42"; - var source = GetDocumentProxyFromSource( sourceType, json ); + var source = GetDocumentFromSource( sourceType, json ); var matches = source.Select( query ).ToList(); var expected = new[] { - source.GetPropertyFromPath( "$" ) + source.FromJsonPathPointer( "$" ) }; - // no consensus - Assert.IsTrue( expected.SequenceEqual( matches ) ); Assert.IsTrue( JsonValueHelper.GetInt32( matches.First() ) == 42 ); } @@ -36,12 +36,12 @@ public void RootOnScalar( string query, Type sourceType ) public void RootOnScalarFalse( string query, Type sourceType ) { const string json = "false"; - var source = GetDocumentProxyFromSource( sourceType, json ); + var source = GetDocumentFromSource( sourceType, json ); var matches = source.Select( query ).ToList(); var expected = new[] { - source.GetPropertyFromPath( "$" ) + source.FromJsonPathPointer( "$" ) }; Assert.IsTrue( expected.SequenceEqual( matches ) ); @@ -54,12 +54,12 @@ public void RootOnScalarFalse( string query, Type sourceType ) public void RootOnScalarTrue( string query, Type sourceType ) { const string json = "true"; - var source = GetDocumentProxyFromSource( sourceType, json ); + var source = GetDocumentFromSource( sourceType, json ); var matches = source.Select( query ).ToList(); var expected = new[] { - source.GetPropertyFromPath( "$" ) + source.FromJsonPathPointer( "$" ) }; Assert.IsTrue( expected.SequenceEqual( matches ) ); diff --git a/test/Hyperbee.Json.Tests/Query/JsonPathUnionTests.cs b/test/Hyperbee.Json.Tests/Query/JsonPathUnionTests.cs index 7fe85818..4ae03251 100644 --- a/test/Hyperbee.Json.Tests/Query/JsonPathUnionTests.cs +++ b/test/Hyperbee.Json.Tests/Query/JsonPathUnionTests.cs @@ -15,22 +15,22 @@ public class JsonPathUnionTests : JsonTestBase [DataRow( "$[0,0]", typeof( JsonNode ) )] public void UnionWithDuplicationFromArray( string query, Type sourceType ) { + // consensus: ["a", "a"] + const string json = """ [ "a" ] """; - var source = GetDocumentProxyFromSource( sourceType, json ); + var source = GetDocumentFromSource( sourceType, json ); var matches = source.Select( query ); var expected = new[] { - source.GetPropertyFromPath( "$[0]" ), - source.GetPropertyFromPath( "$[0]" ) + source.FromJsonPathPointer( "$[0]" ), + source.FromJsonPathPointer( "$[0]" ) }; - // consensus: ["a", "a"] - Assert.IsTrue( expected.SequenceEqual( matches ) ); } @@ -39,22 +39,22 @@ public void UnionWithDuplicationFromArray( string query, Type sourceType ) [DataRow( "$['a','a']", typeof( JsonNode ) )] public void UnionWithDuplicationFromObject( string query, Type sourceType ) { + // consensus: none + const string json = """ { "a": 1 } """; - var source = GetDocumentProxyFromSource( sourceType, json ); + var source = GetDocumentFromSource( sourceType, json ); var matches = source.Select( query ); var expected = new[] { - source.GetPropertyFromPath( "$['a']" ), - source.GetPropertyFromPath( "$['a']" ) + source.FromJsonPathPointer( "$.a" ), + source.FromJsonPathPointer( "$.a" ) }; - // no consensus - Assert.IsTrue( expected.SequenceEqual( matches ) ); } @@ -65,6 +65,8 @@ public void UnionWithDuplicationFromObject( string query, Type sourceType ) [DataRow( "$[?@.key<3,?@.key>6]", typeof( JsonNode ) )] public void UnionWithFilter( string query, Type sourceType ) { + // consensus: none + const string json = """ [ { "key": 1 }, @@ -77,21 +79,19 @@ public void UnionWithFilter( string query, Type sourceType ) { "key": 4 } ] """; - var source = GetDocumentProxyFromSource( sourceType, json ); + var source = GetDocumentFromSource( sourceType, json ); var matches = source.Select( query ); var expected = new[] { - source.GetPropertyFromPath( "$[0]" ), // key: 1 - source.GetPropertyFromPath( "$[5]" ), // key: 2 + source.FromJsonPathPointer( "$[0]" ), // key: 1 + source.FromJsonPathPointer( "$[5]" ), // key: 2 - source.GetPropertyFromPath( "$[1]" ), // key: 8 - source.GetPropertyFromPath( "$[3]" ), // key: 10 - source.GetPropertyFromPath( "$[4]" ) // key: 7 + source.FromJsonPathPointer( "$[1]" ), // key: 8 + source.FromJsonPathPointer( "$[3]" ), // key: 10 + source.FromJsonPathPointer( "$[4]" ) // key: 7 }; - // no consensus - Assert.IsTrue( expected.SequenceEqual( matches ) ); } @@ -100,23 +100,23 @@ public void UnionWithFilter( string query, Type sourceType ) [DataRow( "$['key','another']", typeof( JsonNode ) )] public void UnionWithKeys( string query, Type sourceType ) { + // consensus: ["value", "entry"] + const string json = """ { "key": "value", "another": "entry" } """; - var source = GetDocumentProxyFromSource( sourceType, json ); + var source = GetDocumentFromSource( sourceType, json ); var matches = source.Select( query ); var expected = new[] { - source.GetPropertyFromPath( "$['key']" ), - source.GetPropertyFromPath( "$['another']" ) + source.FromJsonPathPointer( "$.key" ), + source.FromJsonPathPointer( "$.another" ) }; - // consensus: ["value", "entry"] - Assert.IsTrue( expected.SequenceEqual( matches ) ); } @@ -125,6 +125,8 @@ public void UnionWithKeys( string query, Type sourceType ) [DataRow( "$['key','another','thing1']", typeof( JsonNode ) )] public void UnionWithMultipleKeys( string query, Type sourceType ) { + // consensus: ["value", "entry"] + const string json = """ { "key": "value", @@ -132,18 +134,71 @@ public void UnionWithMultipleKeys( string query, Type sourceType ) "thing1": "thing2" } """; - var source = GetDocumentProxyFromSource( sourceType, json ); + var source = GetDocumentFromSource( sourceType, json ); var matches = source.Select( query ); var expected = new[] { - source.GetPropertyFromPath( "$['key']" ), - source.GetPropertyFromPath( "$['another']" ), - source.GetPropertyFromPath( "$['thing1']" ) + source.FromJsonPathPointer( "$.key" ), + source.FromJsonPathPointer( "$.another" ), + source.FromJsonPathPointer( "$.thing1" ) }; - // consensus: ["value", "entry"] - Assert.IsTrue( expected.SequenceEqual( matches ) ); } + + [DataTestMethod] + [DataRow( "$..['c','d']", typeof( JsonDocument ) )] + [DataRow( "$..['c','d']", typeof( JsonNode ) )] + public void UnionWithKeysAfterRecursiveDescent( string query, Type sourceType ) + { + // consensus: ["cc1", "cc2", "cc3", "cc5", "dd1", "dd2", "dd4"] + // any order + + const string json = """ + [ + { + "c": "cc1", + "d": "dd1", + "e": "ee1" + }, + { + "c": "cc2", + "child": { + "d": "dd2" + } + }, + { + "c": "cc3" + }, + { + "d": "dd4" + }, + { + "child": { + "c": "cc5" + } + } + ] + """; + + var source = GetDocumentFromSource( sourceType, json ); + + var matches = source.Select( query ); + var expected = new[] + { + source.FromJsonPathPointer( "$[0].c" ), + source.FromJsonPathPointer( "$[0].d" ), + source.FromJsonPathPointer( "$[1].c" ), + source.FromJsonPathPointer( "$[1].child.d" ), + source.FromJsonPathPointer( "$[2].c" ), + source.FromJsonPathPointer( "$[3].d" ), + source.FromJsonPathPointer( "$[4].child.c" ) + + }; + + var equals = matches.SequenceEqual( expected ); + + Assert.IsTrue( equals ); + } } diff --git a/test/Hyperbee.Json.Tests/TestDocuments/JsonPath.json b/test/Hyperbee.Json.Tests/TestDocuments/BookStore.json similarity index 100% rename from test/Hyperbee.Json.Tests/TestDocuments/JsonPath.json rename to test/Hyperbee.Json.Tests/TestDocuments/BookStore.json diff --git a/test/Hyperbee.Json.Tests/TestSupport/IJsonPathProxy.cs b/test/Hyperbee.Json.Tests/TestSupport/IJsonPathProxy.cs deleted file mode 100644 index 8f62d9ac..00000000 --- a/test/Hyperbee.Json.Tests/TestSupport/IJsonPathProxy.cs +++ /dev/null @@ -1,11 +0,0 @@ -using System.Collections.Generic; - -namespace Hyperbee.Json.Tests.TestSupport; - -public interface IJsonPathProxy -{ - object Source { get; } - IEnumerable Select( string query ); - dynamic GetPropertyFromPath( string pathLiteral ); - IEnumerable ArrayEmpty { get; } -} diff --git a/test/Hyperbee.Json.Tests/TestSupport/IJsonPathSource.cs b/test/Hyperbee.Json.Tests/TestSupport/IJsonPathSource.cs new file mode 100644 index 00000000..5502fd9b --- /dev/null +++ b/test/Hyperbee.Json.Tests/TestSupport/IJsonPathSource.cs @@ -0,0 +1,9 @@ +using System.Collections.Generic; + +namespace Hyperbee.Json.Tests.TestSupport; + +public interface IJsonPathSource +{ + IEnumerable Select( string query ); + dynamic FromJsonPathPointer( string pathLiteral ); +} diff --git a/test/Hyperbee.Json.Tests/TestSupport/JsonDocumentProxy.cs b/test/Hyperbee.Json.Tests/TestSupport/JsonDocumentProxy.cs deleted file mode 100644 index b98f9b76..00000000 --- a/test/Hyperbee.Json.Tests/TestSupport/JsonDocumentProxy.cs +++ /dev/null @@ -1,16 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text.Json; -using Hyperbee.Json.Extensions; - -namespace Hyperbee.Json.Tests.TestSupport; - -public class JsonDocumentProxy( string source ) : IJsonPathProxy -{ - protected JsonDocument Internal { get; set; } = JsonDocument.Parse( source ); - public object Source => Internal; - public IEnumerable Select( string query ) => Internal.Select( query ).Cast(); - public dynamic GetPropertyFromPath( string pathLiteral ) => Internal.RootElement.GetPropertyFromPath( pathLiteral ); - public IEnumerable ArrayEmpty => Array.Empty().Cast(); -} diff --git a/test/Hyperbee.Json.Tests/TestSupport/JsonDocumentSource.cs b/test/Hyperbee.Json.Tests/TestSupport/JsonDocumentSource.cs new file mode 100644 index 00000000..f8150ede --- /dev/null +++ b/test/Hyperbee.Json.Tests/TestSupport/JsonDocumentSource.cs @@ -0,0 +1,13 @@ +using System.Collections.Generic; +using System.Linq; +using System.Text.Json; +using Hyperbee.Json.Extensions; + +namespace Hyperbee.Json.Tests.TestSupport; + +public class JsonDocumentSource( string source ) : IJsonPathSource +{ + private JsonDocument Document { get; } = JsonDocument.Parse( source ); + public IEnumerable Select( string query ) => Document.Select( query ).Cast(); + public dynamic FromJsonPathPointer( string pathLiteral ) => Document.RootElement.FromJsonPathPointer( pathLiteral ); +} diff --git a/test/Hyperbee.Json.Tests/TestSupport/JsonNodeProxy.cs b/test/Hyperbee.Json.Tests/TestSupport/JsonNodeProxy.cs deleted file mode 100644 index d2e96ad2..00000000 --- a/test/Hyperbee.Json.Tests/TestSupport/JsonNodeProxy.cs +++ /dev/null @@ -1,16 +0,0 @@ -using System.Collections.Generic; -using System.Text.Json.Nodes; -using Hyperbee.Json.Extensions; - -namespace Hyperbee.Json.Tests.TestSupport; - -public class JsonNodeProxy( string source ) : IJsonPathProxy -{ - protected JsonNode Internal { get; set; } = JsonNode.Parse( source ); - public object Source => Internal; - public IEnumerable Select( string query ) => Internal.Select( query ); - - public dynamic GetPropertyFromPath( string pathLiteral ) => Internal.GetPropertyFromPath( pathLiteral ); - - public IEnumerable ArrayEmpty => []; -} diff --git a/test/Hyperbee.Json.Tests/TestSupport/JsonNodeSource.cs b/test/Hyperbee.Json.Tests/TestSupport/JsonNodeSource.cs new file mode 100644 index 00000000..72e35878 --- /dev/null +++ b/test/Hyperbee.Json.Tests/TestSupport/JsonNodeSource.cs @@ -0,0 +1,13 @@ +using System.Collections.Generic; +using System.Text.Json.Nodes; +using Hyperbee.Json.Extensions; + +namespace Hyperbee.Json.Tests.TestSupport; + +public class JsonNodeSource( string source ) : IJsonPathSource +{ + private JsonNode Document { get; } = JsonNode.Parse( source ); + public IEnumerable Select( string query ) => Document.Select( query ); + + public dynamic FromJsonPathPointer( string pathLiteral ) => Document.FromJsonPathPointer( pathLiteral ); +} diff --git a/test/Hyperbee.Json.Tests/TestSupport/JsonTestBase.cs b/test/Hyperbee.Json.Tests/TestSupport/JsonTestBase.cs index 7e393c98..4c6231ae 100644 --- a/test/Hyperbee.Json.Tests/TestSupport/JsonTestBase.cs +++ b/test/Hyperbee.Json.Tests/TestSupport/JsonTestBase.cs @@ -8,19 +8,7 @@ namespace Hyperbee.Json.Tests.TestSupport; public class JsonTestBase { - protected static string DocumentDefault { get; set; } = "JsonPath.json"; - - protected static JsonDocument ReadJsonDocument( string filename = null ) - { - using var stream = GetManifestStream( filename ); - return JsonDocument.Parse( stream! ); - } - - protected static JsonNode ReadJsonNode( string filename = null ) - { - using var stream = GetManifestStream( filename ); - return JsonNode.Parse( stream! ); - } + protected static string DocumentDefault { get; set; } = "BookStore.json"; protected static string ReadJsonString( string filename = null ) { @@ -42,39 +30,33 @@ public static TType GetDocument( string filename = null ) { var type = typeof( TType ); - if ( type == typeof( JsonDocument ) ) - return (TType) (object) ReadJsonDocument( filename ); - - if ( type == typeof( JsonNode ) ) - return (TType) (object) ReadJsonNode( filename ); + var stream = GetManifestStream( filename ); - throw new NotSupportedException(); - } + if ( type == typeof( JsonDocument ) ) + return (TType) (object) JsonDocument.Parse( stream! ); - public static object GetDocument( Type target, string filename = null ) - { - if ( target == typeof( JsonDocument ) ) - return GetDocument( filename ); + if ( type == typeof( JsonElement ) ) + return (TType) (object) JsonDocument.Parse( stream! ).RootElement; - if ( target == typeof( JsonNode ) ) - return GetDocument( filename ); + if ( type == typeof( JsonNode ) ) + return (TType) (object) JsonNode.Parse( stream! ); throw new NotSupportedException(); } - public static IJsonPathProxy GetDocumentProxy( Type target, string filename = null ) + public static IJsonPathSource GetDocumentFromResource( Type target, string filename = null ) { var source = ReadJsonString( filename ); - return GetDocumentProxyFromSource( target, source ); + return GetDocumentFromSource( target, source ); } - public static IJsonPathProxy GetDocumentProxyFromSource( Type target, string source ) + public static IJsonPathSource GetDocumentFromSource( Type target, string source ) { if ( target == typeof( JsonDocument ) ) - return new JsonDocumentProxy( source ); + return new JsonDocumentSource( source ); if ( target == typeof( JsonNode ) ) - return new JsonNodeProxy( source ); + return new JsonNodeSource( source ); throw new NotSupportedException(); } diff --git a/test/Hyperbee.Json.Tests/TestSupport/JsonValueHelper.cs b/test/Hyperbee.Json.Tests/TestSupport/JsonValueHelper.cs index 6dee39c2..f1156e69 100644 --- a/test/Hyperbee.Json.Tests/TestSupport/JsonValueHelper.cs +++ b/test/Hyperbee.Json.Tests/TestSupport/JsonValueHelper.cs @@ -1,14 +1,13 @@ -using System.Text.Json; +using System; +using System.Text.Json; using System.Text.Json.Nodes; -using System.Text.RegularExpressions; namespace Hyperbee.Json.Tests.TestSupport; -// test helper to assist with getting, and normalizing, json values +// Test helper to assist with getting, and normalizing, json values // // JsonElement and JsonNode return values differently. -// this helper provides a common interface for value -// retrieval that simplifies unit testing. +// Provide a common interface for value retrieval to simplify unit tests. internal static class JsonValueHelper { @@ -20,13 +19,11 @@ internal static class JsonValueHelper public static string GetString( JsonElement value, bool minify = false ) { - if ( value.ValueKind == JsonValueKind.Object || value.ValueKind == JsonValueKind.Array ) - { - var result = value.ToString(); - return minify ? MinifyJsonString( result ) : result; - } + if ( value.ValueKind != JsonValueKind.Object && value.ValueKind != JsonValueKind.Array ) + return value.GetString(); - return value.GetString(); + var result = value.ToString(); + return minify ? MinifyJson( result ) : result; } // JsonNode Values @@ -37,25 +34,57 @@ public static string GetString( JsonElement value, bool minify = false ) public static string GetString( JsonNode value, bool minify = false ) { - if ( value is JsonObject || value is JsonArray ) - { - var options = new JsonSerializerOptions - { - WriteIndented = false - }; + if ( value is not JsonObject && value is not JsonArray ) + return value.AsValue().GetValue(); - var result = value.ToJsonString( options ); - return minify ? MinifyJsonString( result ) : result; - } + var options = new JsonSerializerOptions { WriteIndented = false }; - return value.AsValue().GetValue(); + var result = value.ToJsonString( options ); + return minify ? MinifyJson( result ) : result; } // Json string helpers - public static string MinifyJsonString( string json ) + public static string MinifyJson( ReadOnlySpan input ) { - const string minifyPattern = "(\"(?:[^\"\\\\]|\\\\.)*\")|\\s+"; - return Regex.Replace( json, minifyPattern, "$1" ); + Span buffer = new char[input.Length]; + int bufferIndex = 0; + bool insideString = false; + bool escapeNext = false; + + foreach ( char ch in input ) + { + switch ( ch ) + { + case '\\': + if ( insideString ) escapeNext = !escapeNext; + buffer[bufferIndex++] = ch; + break; + + case '\"': + if ( !escapeNext ) + insideString = !insideString; + + escapeNext = false; + buffer[bufferIndex++] = ch; + break; + + case '\r': + case '\n': + case '\t': + case ' ': + if ( insideString ) + buffer[bufferIndex++] = ch; + + break; + + default: + escapeNext = false; + buffer[bufferIndex++] = ch; + break; + } + } + + return new string( buffer[..bufferIndex] ); } }