diff --git a/README.md b/README.md index 4e5b6551..fc81a9ee 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ A C# implementation of JSONPath for .NET `System.Text.Json` and `System.Text.Jso .NET `System.Text.Json` lacks support for JSONPath. The primary goal of this project is to provide a JSONPath library for .NET that will -* Directly leverage `System.Text.Json` +* Directly leverage `System.Text.Json` and `System.Text.Json.Nodes` * Align with the draft JSONPath Specification RFC 9535 * [Working Draft](https://github.com/ietf-wg-jsonpath/draft-ietf-jsonpath-base). * [Editor Copy](https://ietf-wg-jsonpath.github.io/draft-ietf-jsonpath-base/draft-ietf-jsonpath-base.html) @@ -36,8 +36,7 @@ JSONPath allows the wildcard symbol `*` for member names and array indices. It borrows the descendant operator `..` from [E4X][e4x] and the array slice syntax proposal `[start:end:step]` from ECMASCRIPT 4. -Expressions of the underlying scripting language (``) can be used as an -alternative to explicit names or indices, as in: +Expressions can be used as an alternative to explicit names or indices, as in: $.store.book[(@.length-1)].title @@ -197,12 +196,24 @@ the `TryReadValueHandler` on the converter. This handler will allow you to inter numeric values during the deserialization process. ### Equality Helpers -* `JsonElement.DeepEquals` -* `JsonElementEqualityDeepComparer` -* `JsonElementPositionComparer` + +| Method | Description +|:-----------------------------------|:----------- +| `JsonElement.DeepEquals` | Performs a deep equals comparison +| `JsonElementEqualityDeepComparer` | A deep equals equality comparer +| `JsonElementPositionComparer` | A position comparer that compares position in the backing stream ### Property Diving -* `JsonElement.GetPropertyFromKey` + +| Method | Description +|:-----------------------------------|:----------- +| `JsonElement.GetPropertyFromKey` | Dives for properties using absolute keys like `$['store']['book'][2]['author']` + +### JsonElement Helpers + +| Method | Description +|:-----------------------------------|:----------- +| `JsonPathBuilder` | Returns the absolute JsonPath string for a given element ## Acknowlegements diff --git a/src/Hyperbee.Json/Evaluators/IJsonPathFilterEvaluator.cs b/src/Hyperbee.Json/Evaluators/IJsonPathFilterEvaluator.cs index 71ba55d9..bfd3c7f0 100644 --- a/src/Hyperbee.Json/Evaluators/IJsonPathFilterEvaluator.cs +++ b/src/Hyperbee.Json/Evaluators/IJsonPathFilterEvaluator.cs @@ -1,9 +1,7 @@  namespace Hyperbee.Json.Evaluators; -public delegate object JsonPathEvaluator( string filter, TType current, TType root ); - public interface IJsonPathFilterEvaluator { - public object Evaluator( string filter, TType current, TType root ); + public object Evaluate( string filter, TType current, TType root ); } diff --git a/src/Hyperbee.Json/Evaluators/JsonPathExpressionEvaluator.cs b/src/Hyperbee.Json/Evaluators/JsonPathExpressionEvaluator.cs index cc078888..399a86f1 100644 --- a/src/Hyperbee.Json/Evaluators/JsonPathExpressionEvaluator.cs +++ b/src/Hyperbee.Json/Evaluators/JsonPathExpressionEvaluator.cs @@ -1,19 +1,17 @@ using System.Collections.Concurrent; -using System.Text.Json; -using System.Text.Json.Nodes; using Hyperbee.Json.Evaluators.Parser; using Microsoft.CSharp.RuntimeBinder; namespace Hyperbee.Json.Evaluators; -public abstract class JsonPathExpressionEvaluator : IJsonPathFilterEvaluator +public sealed class JsonPathExpressionEvaluator : IJsonPathFilterEvaluator { // ReSharper disable once StaticMemberInGenericType private static readonly ConcurrentDictionary> Compiled = new(); - public object Evaluator( string filter, TType current, TType root ) + public object Evaluate( string filter, TType current, TType root ) { - var compiled = Compiled.GetOrAdd( filter, _ => JsonPathExpression.Compile( filter, this ) ); + var compiled = Compiled.GetOrAdd( filter, _ => JsonPathExpression.Compile( filter ) ); try { @@ -30,5 +28,3 @@ public object Evaluator( string filter, TType current, TType root ) } } -public class JsonPathExpressionElementEvaluator : JsonPathExpressionEvaluator; -public class JsonPathExpressionNodeEvaluator : JsonPathExpressionEvaluator; diff --git a/src/Hyperbee.Json/Evaluators/Parser/Functions/JsonPathCountFunction.cs b/src/Hyperbee.Json/Evaluators/Parser/Functions/JsonPathCountFunction.cs index f3592d0f..61f63f26 100644 --- a/src/Hyperbee.Json/Evaluators/Parser/Functions/JsonPathCountFunction.cs +++ b/src/Hyperbee.Json/Evaluators/Parser/Functions/JsonPathCountFunction.cs @@ -25,21 +25,16 @@ public override Expression GetExpression( string methodName, IList argum { if ( arguments.Count != 1 ) { - return Expression.Block( - Expression.Throw( Expression.Constant( new Exception( $"Invalid use of {Name} function" ) ) ), - Expression.Constant( 0F ) - ); + return Expression.Throw( Expression.Constant( new Exception( $"Invalid use of {Name} function" ) ) ); } var queryExp = Expression.Constant( arguments[0] ); - var evaluatorExp = Expression.Constant( context.Evaluator ); return Expression.Convert( Expression.Call( CountMethod, Expression.Call( JsonPathHelper.SelectMethod, context.Current, context.Root, - queryExp, - evaluatorExp ) ), typeof( float ) ); + queryExp ) ), typeof( float ) ); } } diff --git a/src/Hyperbee.Json/Evaluators/Parser/Functions/JsonPathElementFunction.cs b/src/Hyperbee.Json/Evaluators/Parser/Functions/JsonPathElementFunction.cs index 7d1dbebd..752eb037 100644 --- a/src/Hyperbee.Json/Evaluators/Parser/Functions/JsonPathElementFunction.cs +++ b/src/Hyperbee.Json/Evaluators/Parser/Functions/JsonPathElementFunction.cs @@ -7,9 +7,8 @@ public class JsonPathElementFunction( ParseExpressionContext conte protected override Expression Evaluate( ReadOnlySpan data, ReadOnlySpan item, ref int start, ref int from ) { var queryExp = Expression.Constant( item.ToString() ); - var evaluatorExp = Expression.Constant( context.Evaluator ); - + // Create a call expression for the extension method - return Expression.Call( JsonPathHelper.GetFirstElementValueMethod, context.Current, context.Root, queryExp, evaluatorExp ); + return Expression.Call( JsonPathHelper.GetFirstElementValueMethod, context.Current, context.Root, queryExp ); } } diff --git a/src/Hyperbee.Json/Evaluators/Parser/Functions/JsonPathHelper.cs b/src/Hyperbee.Json/Evaluators/Parser/Functions/JsonPathHelper.cs index 60135081..76aea890 100644 --- a/src/Hyperbee.Json/Evaluators/Parser/Functions/JsonPathHelper.cs +++ b/src/Hyperbee.Json/Evaluators/Parser/Functions/JsonPathHelper.cs @@ -5,7 +5,7 @@ namespace Hyperbee.Json.Evaluators.Parser.Functions; -public static class JsonPathHelper +public static class JsonPathHelper //BF: Is this the right name? JsonPathFilterHelper ? { // ReSharper disable once StaticMemberInGenericType public static readonly MethodInfo GetFirstElementValueMethod; @@ -23,9 +23,9 @@ static JsonPathHelper() { var thisType = typeof( JsonPathHelper ); - GetFirstElementValueMethod = thisType.GetMethod( nameof( GetFirstElementValue ), [typeof( TType ), typeof( TType ), typeof( string ), typeof( IJsonPathFilterEvaluator )] ); - GetFirstElementMethod = thisType.GetMethod( nameof( GetFirstElement ), [typeof( TType ), typeof( TType ), typeof( string ), typeof( IJsonPathFilterEvaluator )] ); - SelectMethod = thisType.GetMethod( nameof( Select ), [typeof( TType ), typeof( TType ), typeof( string ), typeof( IJsonPathFilterEvaluator )] ); + GetFirstElementValueMethod = thisType.GetMethod( nameof( GetFirstElementValue ), [typeof( TType ), typeof( TType ), typeof( string ) ] ); + GetFirstElementMethod = thisType.GetMethod( nameof( GetFirstElement ), [typeof( TType ), typeof( TType ), typeof( string ) ] ); + SelectMethod = thisType.GetMethod( nameof( Select ), [typeof( TType ), typeof( TType ), typeof( string ) ] ); IsTruthyMethod = thisType.GetMethod( nameof( IsTruthy ) ); } @@ -65,9 +65,9 @@ private static bool IsNotEmpty( JsonNode node ) }; } - public static object GetFirstElementValue( JsonElement current, JsonElement root, string query, IJsonPathFilterEvaluator evaluator ) + public static object GetFirstElementValue( JsonElement current, JsonElement root, string query ) { - var first = GetFirstElement( current, root, query, evaluator ); + var first = GetFirstElement( current, root, query ); return first.ValueKind switch { @@ -83,9 +83,9 @@ public static object GetFirstElementValue( JsonElement current, JsonElement root }; } - public static object GetFirstElementValue( JsonNode current, JsonNode root, string query, IJsonPathFilterEvaluator evaluator ) + public static object GetFirstElementValue( JsonNode current, JsonNode root, string query ) { - var first = GetFirstElement( current, root, query, evaluator ); + var first = GetFirstElement( current, root, query ); return first?.GetValueKind() switch { @@ -101,30 +101,31 @@ public static object GetFirstElementValue( JsonNode current, JsonNode root, stri }; } - public static JsonElement GetFirstElement( JsonElement current, JsonElement root, string query, IJsonPathFilterEvaluator evaluator ) + //BF: SelectFirst ? Is visitor optimized for first ? Could these be moved out to just use the extensions ? + + public static JsonElement GetFirstElement( JsonElement current, JsonElement root, string query ) { - return new JsonPath( evaluator ) + return new JsonPath() .Select( current, root, query ) .FirstOrDefault(); } - public static JsonNode GetFirstElement( JsonNode current, JsonNode root, string query, IJsonPathFilterEvaluator evaluator ) + public static JsonNode GetFirstElement( JsonNode current, JsonNode root, string query ) { - return new Nodes.JsonPathNode( evaluator ) + return new Nodes.JsonPathNode() .Select( current, root, query ) .FirstOrDefault(); } - public static IEnumerable Select( JsonElement current, JsonElement root, string query, IJsonPathFilterEvaluator evaluator ) + public static IEnumerable Select( JsonElement current, JsonElement root, string query ) { - return new JsonPath( evaluator ) + return new JsonPath() .Select( current, root, query ); } - public static IEnumerable Select( JsonNode current, JsonNode root, string query, IJsonPathFilterEvaluator evaluator ) + public static IEnumerable Select( JsonNode current, JsonNode root, string query ) { - return new Nodes.JsonPathNode( evaluator ) + return new Nodes.JsonPathNode() .Select( current, root, query ); } - } diff --git a/src/Hyperbee.Json/Evaluators/Parser/Functions/JsonPathLengthFunction.cs b/src/Hyperbee.Json/Evaluators/Parser/Functions/JsonPathLengthFunction.cs index 80d636f7..f8fa1f36 100644 --- a/src/Hyperbee.Json/Evaluators/Parser/Functions/JsonPathLengthFunction.cs +++ b/src/Hyperbee.Json/Evaluators/Parser/Functions/JsonPathLengthFunction.cs @@ -21,22 +21,17 @@ public override Expression GetExpression( string methodName, IList argum { if ( arguments.Count != 1 ) { - return //Expression.Block( - Expression.Throw( Expression.Constant( new ArgumentException( $"{Name} function has invalid parameter count." ) ) );//, - //Expression.Constant( 0F ) - //); + return Expression.Throw( Expression.Constant( new ArgumentException( $"{Name} function has invalid parameter count." ) ) ); } var queryExp = Expression.Constant( arguments[0] ); - var evaluatorExp = Expression.Constant( context.Evaluator ); return Expression.Call( LengthMethod, Expression.Call( JsonPathHelper.GetFirstElementMethod, context.Current, context.Root, - queryExp, - evaluatorExp ) ); + queryExp ) ); } diff --git a/src/Hyperbee.Json/Evaluators/Parser/Functions/JsonPathMatchFunction.cs b/src/Hyperbee.Json/Evaluators/Parser/Functions/JsonPathMatchFunction.cs index 7e961b0e..86474a11 100644 --- a/src/Hyperbee.Json/Evaluators/Parser/Functions/JsonPathMatchFunction.cs +++ b/src/Hyperbee.Json/Evaluators/Parser/Functions/JsonPathMatchFunction.cs @@ -22,23 +22,18 @@ public override Expression GetExpression( string methodName, IList argum { if ( arguments.Count != 2 ) { - return Expression.Block( - Expression.Throw( Expression.Constant( new ArgumentException( $"{Name} function has invalid parameter count." ) ) ), - Expression.Constant( 0F ) - ); + return Expression.Throw( Expression.Constant( new ArgumentException( $"{Name} function has invalid parameter count." ) ) ); } var queryExp = Expression.Constant( arguments[0] ); var regex = Expression.Constant( arguments[1] ); - var evaluatorExp = Expression.Constant( context.Evaluator ); return Expression.Call( MatchMethod, Expression.Call( JsonPathHelper.GetFirstElementMethod, context.Current, context.Root, - queryExp, - evaluatorExp ) + queryExp ) , regex ); } diff --git a/src/Hyperbee.Json/Evaluators/Parser/Functions/JsonPathSearchFunction.cs b/src/Hyperbee.Json/Evaluators/Parser/Functions/JsonPathSearchFunction.cs index 7350b36a..8778f380 100644 --- a/src/Hyperbee.Json/Evaluators/Parser/Functions/JsonPathSearchFunction.cs +++ b/src/Hyperbee.Json/Evaluators/Parser/Functions/JsonPathSearchFunction.cs @@ -22,23 +22,18 @@ public override Expression GetExpression( string methodName, IList argum { if ( arguments.Count != 2 ) { - return Expression.Block( - Expression.Throw( Expression.Constant( new ArgumentException( $"{Name} function has invalid parameter count." ) ) ), - Expression.Constant( 0F ) - ); + return Expression.Throw( Expression.Constant( new ArgumentException( $"{Name} function has invalid parameter count." ) ) ); } var queryExp = Expression.Constant( arguments[0] ); var regex = Expression.Constant( arguments[1] ); - var evaluatorExp = Expression.Constant( context.Evaluator ); return Expression.Call( SearchMethod, Expression.Call( JsonPathHelper.GetFirstElementMethod, context.Current, context.Root, - queryExp, - evaluatorExp ) + queryExp ) , regex ); } diff --git a/src/Hyperbee.Json/Evaluators/Parser/Functions/JsonPathValueFunction.cs b/src/Hyperbee.Json/Evaluators/Parser/Functions/JsonPathValueFunction.cs index 81abe618..20eca5de 100644 --- a/src/Hyperbee.Json/Evaluators/Parser/Functions/JsonPathValueFunction.cs +++ b/src/Hyperbee.Json/Evaluators/Parser/Functions/JsonPathValueFunction.cs @@ -10,20 +10,15 @@ public override Expression GetExpression( string methodName, IList argum { if ( arguments.Count != 1 ) { - return Expression.Block( - Expression.Throw( Expression.Constant( new ArgumentException( $"{Name} function has invalid parameter count." ) ) ), - Expression.Constant( 0F ) - ); + return Expression.Throw( Expression.Constant( new ArgumentException( $"{Name} function has invalid parameter count." ) ) ); } var queryExp = Expression.Constant( arguments[0] ); - var evaluatorExp = Expression.Constant( context.Evaluator ); return Expression.Call( JsonPathHelper.GetFirstElementValueMethod, context.Current, context.Root, - queryExp, - evaluatorExp ); + queryExp ); } } diff --git a/src/Hyperbee.Json/Evaluators/Parser/JsonPathExpression.cs b/src/Hyperbee.Json/Evaluators/Parser/JsonPathExpression.cs index 56bf077e..11d5b347 100644 --- a/src/Hyperbee.Json/Evaluators/Parser/JsonPathExpression.cs +++ b/src/Hyperbee.Json/Evaluators/Parser/JsonPathExpression.cs @@ -16,11 +16,11 @@ public class JsonPathExpression private static readonly MethodInfo ObjectEquals = typeof( object ).GetMethod( "Equals", [typeof( object ), typeof( object )] ); - public static Func Compile( ReadOnlySpan filter, IJsonPathFilterEvaluator evaluator = null ) + public static Func Compile( ReadOnlySpan filter ) { var currentParam = Expression.Parameter( typeof( TType ) ); var rootParam = Expression.Parameter( typeof( TType ) ); - var expressionContext = new ParseExpressionContext( currentParam, rootParam, evaluator ); + var expressionContext = new ParseExpressionContext( currentParam, rootParam ); var expression = Parse( filter, expressionContext ); return Expression diff --git a/src/Hyperbee.Json/Evaluators/Parser/ParseExpressionContext.cs b/src/Hyperbee.Json/Evaluators/Parser/ParseExpressionContext.cs index 47c29961..82e8db4e 100644 --- a/src/Hyperbee.Json/Evaluators/Parser/ParseExpressionContext.cs +++ b/src/Hyperbee.Json/Evaluators/Parser/ParseExpressionContext.cs @@ -2,4 +2,4 @@ namespace Hyperbee.Json.Evaluators.Parser; -public record ParseExpressionContext( Expression Current, Expression Root, IJsonPathFilterEvaluator Evaluator ); +public record ParseExpressionContext( Expression Current, Expression Root ); diff --git a/src/Hyperbee.Json/Extensions/JsonElementExtensions.cs b/src/Hyperbee.Json/Extensions/JsonElementExtensions.cs index 19ea3754..9094e86d 100644 --- a/src/Hyperbee.Json/Extensions/JsonElementExtensions.cs +++ b/src/Hyperbee.Json/Extensions/JsonElementExtensions.cs @@ -7,18 +7,6 @@ namespace Hyperbee.Json.Extensions; public static class JsonElementExtensions { - // Is operations - - public static bool IsNullOrUndefined( this JsonElement value ) - { - return value.ValueKind is JsonValueKind.Null or JsonValueKind.Undefined; - } - - public static bool IsObjectOrArray( this JsonElement value ) - { - return value.ValueKind is JsonValueKind.Array or JsonValueKind.Object; - } - // To operations public static dynamic ToDynamic( this JsonElement value, string path = null ) => new DynamicJsonElement( ref value, path ); diff --git a/src/Hyperbee.Json/Extensions/JsonPathSelectExtensions.cs b/src/Hyperbee.Json/Extensions/JsonPathSelectExtensions.cs index 3cd35cfb..ce07a2f4 100644 --- a/src/Hyperbee.Json/Extensions/JsonPathSelectExtensions.cs +++ b/src/Hyperbee.Json/Extensions/JsonPathSelectExtensions.cs @@ -7,17 +7,17 @@ public static class JsonPathSelectExtensions { public static IEnumerable Select( this JsonElement element, string query ) { - return new JsonPath( null ).Select( element, query ); + return new JsonPath().Select( element, query ); } public static IEnumerable Select( this JsonDocument document, string query ) { - return new JsonPath( null ).Select( document.RootElement, query ); + return new JsonPath().Select( document.RootElement, query ); } public static IEnumerable Select( this JsonNode node, string query ) { - return new Nodes.JsonPathNode( null ).Select( node, query ); + return new Nodes.JsonPathNode().Select( node, query ); } } diff --git a/src/Hyperbee.Json/Extensions/JsonPropertyKeyExtensions.cs b/src/Hyperbee.Json/Extensions/JsonPropertyKeyExtensions.cs index 4fb7a7c9..57aa9f32 100644 --- a/src/Hyperbee.Json/Extensions/JsonPropertyKeyExtensions.cs +++ b/src/Hyperbee.Json/Extensions/JsonPropertyKeyExtensions.cs @@ -20,7 +20,7 @@ public static class JsonPropertyKeyExtensions { public static JsonElement GetPropertyFromKey( this JsonElement jsonElement, ReadOnlySpan propertyPath ) { - if ( jsonElement.IsNullOrUndefined() || propertyPath.IsEmpty ) + if ( IsNullOrUndefined( jsonElement ) || propertyPath.IsEmpty ) return default; var splitter = new JsonPropertyKeySplitter( propertyPath ); @@ -35,11 +35,13 @@ public static JsonElement GetPropertyFromKey( this JsonElement jsonElement, Read jsonElement = jsonElement.TryGetProperty( name!, out var value ) ? value : default; - if ( jsonElement.IsNullOrUndefined() ) + if ( IsNullOrUndefined( jsonElement ) ) return default; } return jsonElement; + + static bool IsNullOrUndefined( JsonElement value ) => value.ValueKind is JsonValueKind.Null or JsonValueKind.Undefined; } public static JsonNode GetPropertyFromKey( this JsonNode jsonNode, ReadOnlySpan propertyPath ) diff --git a/src/Hyperbee.Json/JsonElementInternal.cs b/src/Hyperbee.Json/JsonElementInternal.cs new file mode 100644 index 00000000..13380328 --- /dev/null +++ b/src/Hyperbee.Json/JsonElementInternal.cs @@ -0,0 +1,44 @@ +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 + + var idxField = typeof( JsonElement ).GetField( "_idx", BindingFlags.NonPublic | BindingFlags.Instance ); + + if ( idxField == null ) + throw new MissingFieldException( nameof( JsonElement ), "_idx" ); + + 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 + + var parentField = typeof( JsonElement ).GetField( "_parent", BindingFlags.NonPublic | BindingFlags.Instance ); + + if ( parentField == null ) + throw new MissingFieldException( nameof( JsonElement ), "_parent" ); + + 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/JsonElementPositionComparer.cs b/src/Hyperbee.Json/JsonElementPositionComparer.cs index 29118972..85cdd4df 100644 --- a/src/Hyperbee.Json/JsonElementPositionComparer.cs +++ b/src/Hyperbee.Json/JsonElementPositionComparer.cs @@ -1,16 +1,16 @@ -using System.Reflection; -using System.Reflection.Emit; -using System.Text.Json; +using System.Text.Json; namespace Hyperbee.Json; internal class JsonElementPositionComparer : IEqualityComparer { - internal static readonly Func GetIdx; - internal static readonly Func GetParent; - - static JsonElementPositionComparer() + public bool Equals( JsonElement x, JsonElement y ) { + // check for quick out + + if ( x.ValueKind != y.ValueKind ) + return false; + // 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. @@ -22,65 +22,27 @@ static JsonElementPositionComparer() // // These arguments are stored in private fields and are not exposed. While note ideal, we // will access these fields through dynamic methods to use for our comparison. - - // Create DynamicMethod for _idx field - - var idxField = typeof( JsonElement ).GetField( "_idx", BindingFlags.NonPublic | BindingFlags.Instance ); - - if ( idxField == null ) - throw new MissingFieldException( nameof( JsonElement ), "_idx" ); - - var getIdxDynamicMethod = new DynamicMethod( "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 - - var parentField = typeof( JsonElement ).GetField( "_parent", BindingFlags.NonPublic | BindingFlags.Instance ); - - if ( parentField == null ) - throw new MissingFieldException( nameof( JsonElement ), "_parent" ); - - var getParentDynamicMethod = new DynamicMethod( "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 ) ); - } - - public bool Equals( JsonElement x, JsonElement y ) - { - // check for quick out - - if ( x.ValueKind != y.ValueKind ) - return false; - + // check parent documents // BF: 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 = GetParent( x ); - var yParent = GetParent( y ); + var xParent = JsonElementInternal.GetParent( x ); + var yParent = JsonElementInternal.GetParent( y ); if ( !ReferenceEquals( xParent, yParent ) ) return false; // check idx values - return GetIdx( x ) == GetIdx( y ); + return JsonElementInternal.GetIdx( x ) == JsonElementInternal.GetIdx( y ); } public int GetHashCode( JsonElement obj ) { - var parent = GetParent( obj ); - var idx = GetIdx( obj ); + var parent = JsonElementInternal.GetParent( obj ); + var idx = JsonElementInternal.GetIdx( obj ); return HashCode.Combine( parent, idx ); } diff --git a/src/Hyperbee.Json/JsonPath.cs b/src/Hyperbee.Json/JsonPath.cs index 700718ec..d25d9303 100644 --- a/src/Hyperbee.Json/JsonPath.cs +++ b/src/Hyperbee.Json/JsonPath.cs @@ -1,88 +1,21 @@ -#region License -// C# Implementation of JSONPath[1] -// -// [1] http://goessner.net/articles/JsonPath/ -// [2] https://github.com/atifaziz/JSONPath -// -// The MIT License -// -// Copyright (c) 2019 Brenton Farmer. All rights reserved. -// Portions Copyright (c) 2007 Atif Aziz. All rights reserved. -// Portions Copyright (c) 2007 Stefan Goessner (goessner.net) -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -// THE SOFTWARE. -// -#endregion - -using System.Text.Json; +using System.Text.Json; using Hyperbee.Json.Evaluators; -using Hyperbee.Json.Tokenizer; namespace Hyperbee.Json; -// https://ietf-wg-jsonpath.github.io/draft-ietf-jsonpath-base/draft-ietf-jsonpath-base.html -// https://github.com/ietf-wg-jsonpath/draft-ietf-jsonpath-base public sealed class JsonPath { - public static IJsonPathFilterEvaluator DefaultEvaluator { get; set; } = new JsonPathExpressionElementEvaluator(); - private readonly IJsonPathFilterEvaluator _evaluator; - - private readonly JsonDocumentPathVisitor _visitor = new(); - - // ctor - - public JsonPath() - : this( null ) - { - } - - public JsonPath( IJsonPathFilterEvaluator evaluator ) - { - _evaluator = evaluator ?? DefaultEvaluator ?? new JsonPathExpressionElementEvaluator(); - } + public static IJsonPathFilterEvaluator FilterEvaluator { get; set; } = new JsonPathExpressionEvaluator(); + + private readonly JsonPathVisitorBase _visitor = new JsonPathElementVisitor(); public IEnumerable Select( in JsonElement value, string query ) { - return Select( value, value, query ); + return _visitor.ExpressionVisitor( value, value, query, FilterEvaluator ); } internal IEnumerable Select( in JsonElement value, JsonElement root, string query ) { - if ( string.IsNullOrWhiteSpace( query ) ) - throw new ArgumentNullException( nameof( query ) ); - - // quick out - - if ( query == "$" ) - return [value]; - - // tokenize - - var tokens = JsonPathQueryTokenizer.Tokenize( query ); - - if ( !tokens.IsEmpty ) - { - var firstToken = tokens.Peek().Selectors.First().Value; - if ( firstToken == "$" || firstToken == "@" ) - tokens = tokens.Pop(); - } - - return _visitor.ExpressionVisitor( new JsonDocumentPathVisitor.VisitorArgs( value, root, tokens ), _evaluator.Evaluator ); + return _visitor.ExpressionVisitor( value, root, query, FilterEvaluator ); } } diff --git a/src/Hyperbee.Json/JsonPathBuilder.cs b/src/Hyperbee.Json/JsonPathBuilder.cs index 69d20253..3fd32cbf 100644 --- a/src/Hyperbee.Json/JsonPathBuilder.cs +++ b/src/Hyperbee.Json/JsonPathBuilder.cs @@ -1,4 +1,4 @@ -using System.Text.Json; +using System.Text.Json; namespace Hyperbee.Json; @@ -20,14 +20,14 @@ public JsonPathBuilder( JsonElement rootElement ) // we will avoid allocating full paths for every node by // building a dictionary cache of (parentId, segment) pairs. - _parentMap[GetIdx( _rootElement )] = (-1, "$"); // seed parent map with root + _parentMap[GetUniqueId( _rootElement )] = (-1, "$"); // seed parent map with root } public string GetPath( JsonElement targetElement ) { // quick out - var targetId = GetIdx( targetElement ); + var targetId = GetUniqueId( targetElement ); if ( _parentMap.ContainsKey( targetId ) ) return BuildPath( targetId, _parentMap ); @@ -39,7 +39,7 @@ public string GetPath( JsonElement targetElement ) while ( stack.Count > 0 ) { var currentElement = stack.Pop(); - var elementId = GetIdx( currentElement ); + var elementId = GetUniqueId( currentElement ); if ( _comparer.Equals( currentElement, targetElement ) ) return BuildPath( elementId, _parentMap ); @@ -49,7 +49,7 @@ public string GetPath( JsonElement targetElement ) case JsonValueKind.Object: foreach ( var property in currentElement.EnumerateObject() ) { - var childElementId = GetIdx( property.Value ); + var childElementId = GetUniqueId( property.Value ); if ( !_parentMap.ContainsKey( childElementId ) ) _parentMap[childElementId] = (elementId, $".{property.Name}"); @@ -62,7 +62,7 @@ public string GetPath( JsonElement targetElement ) var arrayIdx = 0; foreach ( var element in currentElement.EnumerateArray() ) { - var childElementId = GetIdx( element ); + var childElementId = GetUniqueId( element ); if ( !_parentMap.ContainsKey( childElementId ) ) _parentMap[childElementId] = (elementId, $"[{arrayIdx}]"); @@ -77,9 +77,9 @@ public string GetPath( JsonElement targetElement ) return null; // target not found } - private static int GetIdx( JsonElement element ) + private static int GetUniqueId( JsonElement element ) { - return JsonElementPositionComparer.GetIdx( element ); // Not ideal, but neither is creating multiple dynamic methods. Discuss how to handle. + return JsonElementInternal.GetIdx( element ); } private static string BuildPath( int elementId, Dictionary parentMap ) @@ -96,44 +96,4 @@ private static string BuildPath( int elementId, Dictionary( 4 ); - stack.Push( (_rootElement, "$") ); - - while ( stack.Count > 0 ) - { - var (currentElement, currentPath) = stack.Pop(); - - if ( _comparer.Equals( currentElement, targetElement ) ) - return currentPath; - - switch ( currentElement.ValueKind ) - { - case JsonValueKind.Object: - foreach ( var property in currentElement.EnumerateObject() ) - { - var newPath = $"{currentPath}.{property.Name}"; - stack.Push( (property.Value, newPath) ); - } - - break; - - case JsonValueKind.Array: - var index = 0; - foreach ( var element in currentElement.EnumerateArray() ) - { - var newPath = $"{currentPath}[{index++}]"; - stack.Push( (element, newPath) ); - } - - break; - } - } - - return null; - } - */ } diff --git a/src/Hyperbee.Json/JsonDocumentPathVisitor.cs b/src/Hyperbee.Json/JsonPathElementVisitor.cs similarity index 89% rename from src/Hyperbee.Json/JsonDocumentPathVisitor.cs rename to src/Hyperbee.Json/JsonPathElementVisitor.cs index bdf0dfac..8e1eaa8b 100644 --- a/src/Hyperbee.Json/JsonDocumentPathVisitor.cs +++ b/src/Hyperbee.Json/JsonPathElementVisitor.cs @@ -5,7 +5,7 @@ namespace Hyperbee.Json; -internal class JsonDocumentPathVisitor : JsonPathVisitorBase +public class JsonPathElementVisitor : JsonPathVisitorBase { internal override IEnumerable<(JsonElement, string)> EnumerateChildValues( JsonElement value ) { @@ -22,7 +22,7 @@ internal class JsonDocumentPathVisitor : JsonPathVisitorBase } case JsonValueKind.Object: { - foreach ( var result in ProcessProperties( value.EnumerateObject() ) ) + foreach ( var result in ReverseProperties( value.EnumerateObject() ) ) yield return result; break; @@ -31,7 +31,7 @@ internal class JsonDocumentPathVisitor : JsonPathVisitorBase yield break; - static IEnumerable<(JsonElement, string)> ProcessProperties( JsonElement.ObjectEnumerator enumerator ) + static IEnumerable<(JsonElement, string)> ReverseProperties( JsonElement.ObjectEnumerator enumerator ) { if ( !enumerator.MoveNext() ) { @@ -40,7 +40,7 @@ internal class JsonDocumentPathVisitor : JsonPathVisitorBase var property = enumerator.Current; - foreach ( var result in ProcessProperties( enumerator ) ) + foreach ( var result in ReverseProperties( enumerator ) ) { yield return result; } @@ -58,9 +58,10 @@ internal override JsonElement GetElementAt( JsonElement value, int index ) [MethodImpl( MethodImplOptions.AggressiveInlining )] internal override bool IsObjectOrArray( JsonElement value ) { - return value.IsObjectOrArray(); + return value.ValueKind is JsonValueKind.Array or JsonValueKind.Object; } + [MethodImpl( MethodImplOptions.AggressiveInlining )] internal override bool IsArray( JsonElement value, out int length ) { if ( value.ValueKind == JsonValueKind.Array ) diff --git a/src/Hyperbee.Json/JsonPathVisitorBase.cs b/src/Hyperbee.Json/JsonPathVisitorBase.cs index 98ad5ecb..b9902771 100644 --- a/src/Hyperbee.Json/JsonPathVisitorBase.cs +++ b/src/Hyperbee.Json/JsonPathVisitorBase.cs @@ -1,4 +1,38 @@ -using System.Collections.Immutable; +#region License + +// C# Implementation of JSONPath[1] +// +// [1] http://goessner.net/articles/JsonPath/ +// [2] https://github.com/atifaziz/JSONPath +// +// The MIT License +// +// Copyright (c) 2019 Brenton Farmer. All rights reserved. +// Portions Copyright (c) 2007 Atif Aziz. All rights reserved. +// Portions Copyright (c) 2007 Stefan Goessner (goessner.net) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +#endregion + +using System.Collections.Immutable; using System.Globalization; using Hyperbee.Json.Evaluators; using Hyperbee.Json.Memory; @@ -6,14 +40,42 @@ namespace Hyperbee.Json; +// https://ietf-wg-jsonpath.github.io/draft-ietf-jsonpath-base/draft-ietf-jsonpath-base.html +// https://github.com/ietf-wg-jsonpath/draft-ietf-jsonpath-base + public abstract class JsonPathVisitorBase { - internal IEnumerable ExpressionVisitor( VisitorArgs args, JsonPathEvaluator evaluator ) + internal IEnumerable ExpressionVisitor( in TElement value, in TElement root, string query, IJsonPathFilterEvaluator filterEvaluator ) { - var initialArgs = args; + if ( string.IsNullOrWhiteSpace( query ) ) + throw new ArgumentNullException( nameof(query) ); + + if ( filterEvaluator == null ) + throw new ArgumentNullException( nameof(filterEvaluator) ); + + // quick out + + if ( query == "$" ) + return [value]; - var nodes = new Stack( 4 ); - void PushNode( in TElement v, in IImmutableStack t ) => nodes.Push( new VisitorArgs( v, initialArgs.Root, t ) ); + // tokenize + + var tokens = JsonPathQueryTokenizer.Tokenize( query ); + + if ( !tokens.IsEmpty ) + { + var selector = tokens.Peek().FirstSelector; + + if ( selector == "$" || selector == "@" ) + tokens = tokens.Pop(); + } + + return ExpressionVisitor( root, new VisitorArgs( value, tokens ), filterEvaluator ); + } + + private IEnumerable ExpressionVisitor( TElement root, VisitorArgs args, IJsonPathFilterEvaluator filterEvaluator ) + { + var stack = new Stack( 4 ); do { @@ -24,16 +86,15 @@ internal IEnumerable ExpressionVisitor( VisitorArgs args, JsonPathEval if ( tokens.IsEmpty ) { yield return current; - continue; } // pop the next token from the stack tokens = tokens.Pop( out var token ); - var selector = token.Selectors.First().Value; + var selector = token.FirstSelector; - // make sure we have a container value + // make sure we have a complex value if ( !IsObjectOrArray( current ) ) throw new InvalidOperationException( "Object or Array expected." ); @@ -43,7 +104,7 @@ internal IEnumerable ExpressionVisitor( VisitorArgs args, JsonPathEval if ( token.Singular ) { if ( TryGetChildValue( current, selector, out var childValue ) ) - PushNode( childValue, tokens ); + Push( stack, childValue, tokens ); continue; } @@ -54,7 +115,7 @@ internal IEnumerable ExpressionVisitor( VisitorArgs args, JsonPathEval { foreach ( var (_, childKey) in EnumerateChildValues( current ) ) { - PushNode( current, tokens.Push( new JsonPathToken( childKey, SelectorKind.UnspecifiedSingular ) ) ); // (Dot | Index) + Push( stack, current, tokens.Push( new ( childKey, SelectorKind.UnspecifiedSingular ) ) ); // (Dot | Index) } continue; @@ -67,29 +128,31 @@ internal IEnumerable ExpressionVisitor( VisitorArgs args, JsonPathEval foreach ( var (childValue, _) in EnumerateChildValues( current ) ) { if ( IsObjectOrArray( childValue ) ) - PushNode( childValue, tokens.Push( new JsonPathToken( "..", SelectorKind.UnspecifiedGroup ) ) ); // Descendant + Push( stack, childValue, tokens.Push( new ( "..", SelectorKind.UnspecifiedGroup ) ) ); // Descendant } - PushNode( current, tokens ); + Push( stack, current, tokens ); continue; } // union - - foreach ( var childSelector in token.Selectors.Select( x => x.Value ) ) + + for ( var i = 0; i < token.Selectors.Length; i++ ) // using 'for' for performance { + var childSelector = token.Selectors[i].Value; + // [(exp)] if ( childSelector.Length > 2 && childSelector[0] == '(' && childSelector[^1] == ')' ) { - if ( evaluator( childSelector, current, args.Root ) is not string evalSelector ) + if ( filterEvaluator.Evaluate( childSelector, current, root ) is not string evalSelector ) continue; var selectorKind = evalSelector != "*" && evalSelector != ".." && !JsonPathRegex.RegexSlice().IsMatch( evalSelector ) // (Dot | Index) | Wildcard, Descendant, Slice ? SelectorKind.UnspecifiedSingular : SelectorKind.UnspecifiedGroup; - PushNode( current, tokens.Push( new JsonPathToken( evalSelector, selectorKind ) ) ); + Push( stack, current, tokens.Push( new ( evalSelector, selectorKind ) ) ); continue; } @@ -99,11 +162,11 @@ internal IEnumerable ExpressionVisitor( VisitorArgs args, JsonPathEval { foreach ( var (childValue, childKey) in EnumerateChildValues( current ) ) { - var filter = evaluator( JsonPathRegex.RegexPathFilter().Replace( childSelector, "$1" ), childValue, args.Root ); + var filter = filterEvaluator.Evaluate( JsonPathRegex.RegexPathFilter().Replace( childSelector, "$1" ), childValue, root ); // treat the filter result as truthy if the evaluator returned a non-convertible object instance. if ( filter is not null and not IConvertible || Convert.ToBoolean( filter, CultureInfo.InvariantCulture ) ) - PushNode( current, tokens.Push( new JsonPathToken( childKey, SelectorKind.UnspecifiedSingular ) ) ); // (Name | Index) + Push( stack, current, tokens.Push( new ( childKey, SelectorKind.UnspecifiedSingular ) ) ); // (Name | Index) } continue; @@ -116,7 +179,7 @@ internal IEnumerable ExpressionVisitor( VisitorArgs args, JsonPathEval if ( JsonPathRegex.RegexNumber().IsMatch( childSelector ) ) { // [#,#,...] - PushNode( GetElementAt( current, int.Parse( childSelector ) ), tokens ); + Push( stack, GetElementAt( current, int.Parse( childSelector ) ), tokens ); continue; } @@ -124,13 +187,13 @@ internal IEnumerable ExpressionVisitor( VisitorArgs args, JsonPathEval if ( JsonPathRegex.RegexSlice().IsMatch( childSelector ) ) { foreach ( var index in EnumerateSlice( current, childSelector ) ) - PushNode( GetElementAt( current, index ), tokens ); + Push( stack, GetElementAt( current, index ), tokens ); continue; } // [name1,name2,...] foreach ( var index in EnumerateArrayIndices( length ) ) - PushNode( GetElementAt( current, index ), tokens.Push( new JsonPathToken( childSelector, SelectorKind.UnspecifiedSingular ) ) ); // Name + Push( stack, GetElementAt( current, index ), tokens.Push( new ( childSelector, SelectorKind.UnspecifiedSingular ) ) ); // Name continue; } @@ -144,11 +207,15 @@ internal IEnumerable ExpressionVisitor( VisitorArgs args, JsonPathEval // [name1,name2,...] if ( TryGetChildValue( current, childSelector, out var childValue ) ) - PushNode( childValue, tokens ); + Push( stack, childValue, tokens ); } } - } while ( nodes.TryPop( out args ) ); + } while ( stack.TryPop( out args ) ); + + yield break; + + static void Push( Stack s, in TElement v, in IImmutableStack t ) => s.Push( new VisitorArgs( v, t ) ); } private static IEnumerable EnumerateArrayIndices( int length ) @@ -188,21 +255,17 @@ private IEnumerable EnumerateSlice( TElement value, string sliceExpr ) // abstract methods internal abstract IEnumerable<(TElement, string)> EnumerateChildValues( TElement value ); - internal abstract TElement GetElementAt( TElement value, int index ); - internal abstract bool IsObjectOrArray( TElement current ); internal abstract bool IsArray( TElement current, out int length ); internal abstract bool IsObject( TElement current ); - internal abstract bool TryGetChildValue( in TElement current, ReadOnlySpan childKey, out TElement childValue ); // visitor context - internal sealed class VisitorArgs( in TElement value, in TElement root, in IImmutableStack tokens ) + private sealed class VisitorArgs( in TElement value, in IImmutableStack tokens ) { public readonly TElement Value = value; - public readonly TElement Root = root; public readonly IImmutableStack Tokens = tokens; public void Deconstruct( out TElement value, out IImmutableStack tokens ) diff --git a/src/Hyperbee.Json/Nodes/JsonPath.cs b/src/Hyperbee.Json/Nodes/JsonPath.cs index 40f197bf..1e0852cb 100644 --- a/src/Hyperbee.Json/Nodes/JsonPath.cs +++ b/src/Hyperbee.Json/Nodes/JsonPath.cs @@ -1,90 +1,21 @@ -#region License -// C# Implementation of JSONPath[1] -// -// [1] http://goessner.net/articles/JsonPath/ -// [2] https://github.com/atifaziz/JSONPath -// -// The MIT License -// -// Copyright (c) 2019 Brenton Farmer. All rights reserved. -// Portions Copyright (c) 2007 Atif Aziz. All rights reserved. -// Portions Copyright (c) 2007 Stefan Goessner (goessner.net) -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -// THE SOFTWARE. -// -#endregion - -using System.Text.Json.Nodes; +using System.Text.Json.Nodes; using Hyperbee.Json.Evaluators; -using Hyperbee.Json.Tokenizer; namespace Hyperbee.Json.Nodes; -// https://ietf-wg-jsonpath.github.io/draft-ietf-jsonpath-base/draft-ietf-jsonpath-base.html -// https://github.com/ietf-wg-jsonpath/draft-ietf-jsonpath-base public sealed class JsonPathNode { - public static IJsonPathFilterEvaluator DefaultEvaluator { get; set; } = new JsonPathExpressionNodeEvaluator(); - private readonly IJsonPathFilterEvaluator _evaluator; - - private readonly JsonNodePathVisitor _visitor = new(); - - // ctor - - public JsonPathNode() - : this( null ) - { - } + public static IJsonPathFilterEvaluator FilterEvaluator { get; set; } = new JsonPathExpressionEvaluator(); - public JsonPathNode( IJsonPathFilterEvaluator evaluator ) - { - _evaluator = evaluator ?? DefaultEvaluator ?? new JsonPathExpressionNodeEvaluator(); - } + private readonly JsonPathVisitorBase _visitor = new JsonPathNodeVisitor(); public IEnumerable Select( in JsonNode value, string query ) { - return Select( value, value, query ); + return _visitor.ExpressionVisitor( value, value, query, FilterEvaluator ); } - internal IEnumerable Select( in JsonNode value, in JsonNode root, string query ) + internal IEnumerable Select( in JsonNode value, JsonNode root, string query ) { - if ( string.IsNullOrWhiteSpace( query ) ) - throw new ArgumentNullException( nameof( query ) ); - - // quick out - - if ( query == "$" ) - return [value]; - - // tokenize - - var tokens = JsonPathQueryTokenizer.Tokenize( query ); - - // initiate the expression walk - - if ( !tokens.IsEmpty ) - { - var firstToken = tokens.Peek().Selectors.First().Value; - if ( firstToken == "$" || firstToken == "@" ) - tokens = tokens.Pop(); - } - - return _visitor.ExpressionVisitor( new JsonNodePathVisitor.VisitorArgs( value, root, tokens ), _evaluator.Evaluator ); + return _visitor.ExpressionVisitor( value, root, query, FilterEvaluator ); } } diff --git a/src/Hyperbee.Json/Nodes/JsonNodePathVisitor.cs b/src/Hyperbee.Json/Nodes/JsonPathNodeVisitor.cs similarity index 96% rename from src/Hyperbee.Json/Nodes/JsonNodePathVisitor.cs rename to src/Hyperbee.Json/Nodes/JsonPathNodeVisitor.cs index 7bd376cf..2fcd14ac 100644 --- a/src/Hyperbee.Json/Nodes/JsonNodePathVisitor.cs +++ b/src/Hyperbee.Json/Nodes/JsonPathNodeVisitor.cs @@ -4,7 +4,7 @@ namespace Hyperbee.Json.Nodes; -internal class JsonNodePathVisitor : JsonPathVisitorBase +internal class JsonPathNodeVisitor : JsonPathVisitorBase { internal override IEnumerable<(JsonNode, string)> EnumerateChildValues( JsonNode value ) { @@ -56,6 +56,7 @@ internal override bool IsObjectOrArray( JsonNode value ) return value is JsonObject or JsonArray; } + [MethodImpl( MethodImplOptions.AggressiveInlining )] internal override bool IsArray( JsonNode value, out int length ) { if ( value is JsonArray jsonArray ) diff --git a/src/Hyperbee.Json/Tokenizer/JsonPathToken.cs b/src/Hyperbee.Json/Tokenizer/JsonPathToken.cs index 92ab161b..75c27870 100644 --- a/src/Hyperbee.Json/Tokenizer/JsonPathToken.cs +++ b/src/Hyperbee.Json/Tokenizer/JsonPathToken.cs @@ -16,6 +16,8 @@ internal record JsonPathToken { public SelectorDescriptor[] Selectors { get; init; } + public string FirstSelector => Selectors[0].Value; + public bool Singular { get diff --git a/test/Hyperbee.Json.Benchmark/JsonPathExpressionParser.cs b/test/Hyperbee.Json.Benchmark/JsonPathExpressionParser.cs index a9d6d79c..2c3735f4 100644 --- a/test/Hyperbee.Json.Benchmark/JsonPathExpressionParser.cs +++ b/test/Hyperbee.Json.Benchmark/JsonPathExpressionParser.cs @@ -2,7 +2,6 @@ using System.Text.Json; using System.Text.Json.Nodes; using BenchmarkDotNet.Attributes; -using Hyperbee.Json.Evaluators; using Hyperbee.Json.Evaluators.Parser; namespace Hyperbee.Json.Benchmark; @@ -21,14 +20,11 @@ public void Setup() { _nodeExpressionContext = new ParseExpressionContext( Expression.Parameter( typeof( JsonNode ) ), - Expression.Parameter( typeof( JsonNode ) ), - new JsonPathExpressionNodeEvaluator() ); - + Expression.Parameter( typeof( JsonNode ) ) ); _elementExpressionContext = new ParseExpressionContext( Expression.Parameter( typeof( JsonElement ) ), - Expression.Parameter( typeof( JsonElement ) ), - new JsonPathExpressionElementEvaluator() ); + Expression.Parameter( typeof( JsonElement ) ) ); } [Benchmark] diff --git a/test/Hyperbee.Json.Tests/Builder/JsonPathBuilderTests.cs b/test/Hyperbee.Json.Tests/Builder/JsonPathBuilderTests.cs index 24a8924c..923342ac 100644 --- a/test/Hyperbee.Json.Tests/Builder/JsonPathBuilderTests.cs +++ b/test/Hyperbee.Json.Tests/Builder/JsonPathBuilderTests.cs @@ -25,6 +25,6 @@ public void Should_GetPath( string key, string expected ) var resultCached = builder.GetPath( target ); - Assert.AreEqual( result, expected ); + Assert.AreEqual( resultCached, expected ); } } diff --git a/test/Hyperbee.Json.Tests/Evaluators/JsonPathExpressionTests.cs b/test/Hyperbee.Json.Tests/Evaluators/JsonPathExpressionTests.cs index 6d421896..e6df4bf7 100644 --- a/test/Hyperbee.Json.Tests/Evaluators/JsonPathExpressionTests.cs +++ b/test/Hyperbee.Json.Tests/Evaluators/JsonPathExpressionTests.cs @@ -3,7 +3,6 @@ using System.Linq.Expressions; using System.Text.Json; using System.Text.Json.Nodes; -using Hyperbee.Json.Evaluators; using Hyperbee.Json.Evaluators.Parser; using Hyperbee.Json.Extensions; using Hyperbee.Json.Tests.TestSupport; @@ -154,12 +153,10 @@ private static (Expression, ParameterExpression) GetExpression( string filter, T var expression = sourceType == typeof( JsonElement ) ? JsonPathExpression.Parse( filter, new ParseExpressionContext( param, - param, - new JsonPathExpressionElementEvaluator() ) ) + param ) ) : JsonPathExpression.Parse( filter, new ParseExpressionContext( param, - param, - new JsonPathExpressionNodeEvaluator() ) ); + param ) ); return (expression, param); } @@ -193,7 +190,7 @@ private static bool CompileAndExecute( string filter, Type sourceType ) if ( sourceType == typeof( JsonElement ) ) { var source = GetDocument(); - var func = JsonPathExpression.Compile( filter, new JsonPathExpressionElementEvaluator() ); + var func = JsonPathExpression.Compile( filter ); return func( source.RootElement, source.RootElement ); } @@ -201,7 +198,7 @@ private static bool CompileAndExecute( string filter, Type sourceType ) { // arrange var source = GetDocument(); - var func = JsonPathExpression.Compile( filter, new JsonPathExpressionNodeEvaluator() ); + var func = JsonPathExpression.Compile( filter ); // act return func( source, source );