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] );
}
}