From a846f7d31f6c8e998c20307ca99f02736da58a79 Mon Sep 17 00:00:00 2001 From: Matt Edwards Date: Tue, 11 Jun 2024 14:52:00 -0400 Subject: [PATCH] Refactored use of JsonElement and JsonNode into different implementation or via type accessors. --- .../Evaluators/IJsonTypeDescriptor.cs | 12 + .../Evaluators/IJsonValueAccessor.cs | 11 + ...valuator.cs => JsonPathFilterEvaluator.cs} | 12 +- .../CountElementFunction.cs} | 24 +- .../FilterElementFunction.cs} | 6 +- .../Parser/Element/FilterElementHelper.cs | 61 ++++ .../Element/JsonElementTypeDescriptor.cs | 27 ++ .../Element/JsonElementValueAccessor.cs} | 17 +- .../Parser/Element/LengthElementFunction.cs | 45 +++ .../Parser/Element/MatchElementFunction.cs | 45 +++ .../Parser/Element/SearchElementFunction.cs | 45 +++ .../ValueElementFunction.cs} | 8 +- ...unction.cs => FilterExpressionFunction.cs} | 10 +- .../Evaluators/Parser/FilterFunction.cs | 79 +++++ ...enizerRegex.cs => FilterTokenizerRegex.cs} | 4 +- .../Parser/FilterTruthyExpression.cs | 34 +++ .../Evaluators/Parser/FunctionCreator.cs | 6 + .../Parser/Functions/JsonPathHelper.cs | 131 --------- .../Parser/Functions/JsonPathMatchFunction.cs | 55 ---- .../Parser/Functions/ParserFunction.cs | 102 ------- .../Evaluators/Parser/JsonPathExpression.cs | 21 +- .../Parser/{Functions => }/LiteralFunction.cs | 10 +- .../Parser/Node/CountNodeFunction.cs | 42 +++ .../Parser/Node/FilterNodeFunction.cs | 14 + .../Parser/Node/FilterNodeHelper.cs | 63 ++++ .../Parser/Node/JsonNodeTypeDescriptor.cs | 27 ++ .../Parser/Node/JsonNodeValueAccessor.cs} | 18 +- .../LengthNodeFunction.cs} | 25 +- .../Parser/Node/MatchNodeFunction.cs | 45 +++ .../SearchElementFunction.cs} | 25 +- .../Parser/Node/ValueElementFunction.cs | 24 ++ .../Parser/{Functions => }/ParenFunction.cs | 4 +- .../Parser/ParseExpressionContext.cs | 6 +- .../Extensions/JsonPathSelectExtensions.cs | 7 +- src/Hyperbee.Json/JsonPath.cs | 273 ++++++++++++++++- src/Hyperbee.Json/JsonPathVisitorBase.cs | 277 ------------------ src/Hyperbee.Json/JsonTypeRegistry.cs | 33 +++ src/Hyperbee.Json/Nodes/JsonPath.cs | 21 -- .../Hyperbee.Json.Benchmark.csproj | 1 + .../JsonPathExpressionParser.cs | 16 +- .../JsonPathParseAndSelect.cs | 16 +- .../JsonPathSelectEvaluator.cs | 4 +- .../Evaluators/JsonPathExpressionTests.cs | 16 +- 43 files changed, 1007 insertions(+), 715 deletions(-) create mode 100644 src/Hyperbee.Json/Evaluators/IJsonTypeDescriptor.cs create mode 100644 src/Hyperbee.Json/Evaluators/IJsonValueAccessor.cs rename src/Hyperbee.Json/Evaluators/{JsonPathExpressionEvaluator.cs => JsonPathFilterEvaluator.cs} (71%) rename src/Hyperbee.Json/Evaluators/Parser/{Functions/JsonPathCountFunction.cs => Element/CountElementFunction.cs} (56%) rename src/Hyperbee.Json/Evaluators/Parser/{Functions/JsonPathElementFunction.cs => Element/FilterElementFunction.cs} (51%) create mode 100644 src/Hyperbee.Json/Evaluators/Parser/Element/FilterElementHelper.cs create mode 100644 src/Hyperbee.Json/Evaluators/Parser/Element/JsonElementTypeDescriptor.cs rename src/Hyperbee.Json/{JsonPathElementVisitor.cs => Evaluators/Parser/Element/JsonElementValueAccessor.cs} (82%) create mode 100644 src/Hyperbee.Json/Evaluators/Parser/Element/LengthElementFunction.cs create mode 100644 src/Hyperbee.Json/Evaluators/Parser/Element/MatchElementFunction.cs create mode 100644 src/Hyperbee.Json/Evaluators/Parser/Element/SearchElementFunction.cs rename src/Hyperbee.Json/Evaluators/Parser/{Functions/JsonPathValueFunction.cs => Element/ValueElementFunction.cs} (58%) rename src/Hyperbee.Json/Evaluators/Parser/{Functions/ParserExpressionFunction.cs => FilterExpressionFunction.cs} (51%) create mode 100644 src/Hyperbee.Json/Evaluators/Parser/FilterFunction.cs rename src/Hyperbee.Json/Evaluators/Parser/{Functions/JsonPathFilterTokenizerRegex.cs => FilterTokenizerRegex.cs} (79%) create mode 100644 src/Hyperbee.Json/Evaluators/Parser/FilterTruthyExpression.cs create mode 100644 src/Hyperbee.Json/Evaluators/Parser/FunctionCreator.cs delete mode 100644 src/Hyperbee.Json/Evaluators/Parser/Functions/JsonPathHelper.cs delete mode 100644 src/Hyperbee.Json/Evaluators/Parser/Functions/JsonPathMatchFunction.cs delete mode 100644 src/Hyperbee.Json/Evaluators/Parser/Functions/ParserFunction.cs rename src/Hyperbee.Json/Evaluators/Parser/{Functions => }/LiteralFunction.cs (80%) create mode 100644 src/Hyperbee.Json/Evaluators/Parser/Node/CountNodeFunction.cs create mode 100644 src/Hyperbee.Json/Evaluators/Parser/Node/FilterNodeFunction.cs create mode 100644 src/Hyperbee.Json/Evaluators/Parser/Node/FilterNodeHelper.cs create mode 100644 src/Hyperbee.Json/Evaluators/Parser/Node/JsonNodeTypeDescriptor.cs rename src/Hyperbee.Json/{Nodes/JsonPathNodeVisitor.cs => Evaluators/Parser/Node/JsonNodeValueAccessor.cs} (84%) rename src/Hyperbee.Json/Evaluators/Parser/{Functions/JsonPathLengthFunction.cs => Node/LengthNodeFunction.cs} (52%) create mode 100644 src/Hyperbee.Json/Evaluators/Parser/Node/MatchNodeFunction.cs rename src/Hyperbee.Json/Evaluators/Parser/{Functions/JsonPathSearchFunction.cs => Node/SearchElementFunction.cs} (50%) create mode 100644 src/Hyperbee.Json/Evaluators/Parser/Node/ValueElementFunction.cs rename src/Hyperbee.Json/Evaluators/Parser/{Functions => }/ParenFunction.cs (64%) delete mode 100644 src/Hyperbee.Json/JsonPathVisitorBase.cs create mode 100644 src/Hyperbee.Json/JsonTypeRegistry.cs delete mode 100644 src/Hyperbee.Json/Nodes/JsonPath.cs diff --git a/src/Hyperbee.Json/Evaluators/IJsonTypeDescriptor.cs b/src/Hyperbee.Json/Evaluators/IJsonTypeDescriptor.cs new file mode 100644 index 00000000..783f0c7d --- /dev/null +++ b/src/Hyperbee.Json/Evaluators/IJsonTypeDescriptor.cs @@ -0,0 +1,12 @@ +using Hyperbee.Json.Evaluators.Parser; + +namespace Hyperbee.Json.Evaluators; + +public interface IJsonTypeDescriptor +{ + public Dictionary Functions { get; } + + public IJsonValueAccessor GetAccessor(); + public IJsonPathFilterEvaluator GetFilterEvaluator(); + public FilterFunction GetFilterFunction( ParseExpressionContext context ); +} diff --git a/src/Hyperbee.Json/Evaluators/IJsonValueAccessor.cs b/src/Hyperbee.Json/Evaluators/IJsonValueAccessor.cs new file mode 100644 index 00000000..626c2112 --- /dev/null +++ b/src/Hyperbee.Json/Evaluators/IJsonValueAccessor.cs @@ -0,0 +1,11 @@ +namespace Hyperbee.Json.Evaluators; + +public interface IJsonValueAccessor +{ + IEnumerable<(TElement, string)> EnumerateChildValues( TElement value ); + TElement GetElementAt( TElement value, int index ); + bool IsObjectOrArray( TElement current ); + bool IsArray( TElement current, out int length ); + bool IsObject( TElement current ); + bool TryGetChildValue( in TElement current, ReadOnlySpan childKey, out TElement childValue ); +} diff --git a/src/Hyperbee.Json/Evaluators/JsonPathExpressionEvaluator.cs b/src/Hyperbee.Json/Evaluators/JsonPathFilterEvaluator.cs similarity index 71% rename from src/Hyperbee.Json/Evaluators/JsonPathExpressionEvaluator.cs rename to src/Hyperbee.Json/Evaluators/JsonPathFilterEvaluator.cs index 399a86f1..207dedc3 100644 --- a/src/Hyperbee.Json/Evaluators/JsonPathExpressionEvaluator.cs +++ b/src/Hyperbee.Json/Evaluators/JsonPathFilterEvaluator.cs @@ -4,14 +4,22 @@ namespace Hyperbee.Json.Evaluators; -public sealed class JsonPathExpressionEvaluator : IJsonPathFilterEvaluator +public sealed class JsonPathFilterEvaluator : IJsonPathFilterEvaluator { + private readonly IJsonTypeDescriptor _typeDescriptor; + // ReSharper disable once StaticMemberInGenericType private static readonly ConcurrentDictionary> Compiled = new(); + + public JsonPathFilterEvaluator( IJsonTypeDescriptor typeDescriptor ) + { + _typeDescriptor = typeDescriptor; + } + public object Evaluate( string filter, TType current, TType root ) { - var compiled = Compiled.GetOrAdd( filter, _ => JsonPathExpression.Compile( filter ) ); + var compiled = Compiled.GetOrAdd( filter, _ => JsonPathExpression.Compile( filter, _typeDescriptor ) ); try { diff --git a/src/Hyperbee.Json/Evaluators/Parser/Functions/JsonPathCountFunction.cs b/src/Hyperbee.Json/Evaluators/Parser/Element/CountElementFunction.cs similarity index 56% rename from src/Hyperbee.Json/Evaluators/Parser/Functions/JsonPathCountFunction.cs rename to src/Hyperbee.Json/Evaluators/Parser/Element/CountElementFunction.cs index 61f63f26..10c2dd3c 100644 --- a/src/Hyperbee.Json/Evaluators/Parser/Functions/JsonPathCountFunction.cs +++ b/src/Hyperbee.Json/Evaluators/Parser/Element/CountElementFunction.cs @@ -1,16 +1,17 @@ using System.Linq.Expressions; using System.Reflection; +using System.Text.Json; -namespace Hyperbee.Json.Evaluators.Parser.Functions; +namespace Hyperbee.Json.Evaluators.Parser.Element; -public class JsonPathCountFunction( string methodName, IList arguments, ParseExpressionContext context ) : ParserExpressionFunction( methodName, arguments, context ) +public class CountElementFunction( string methodName, IList arguments, ParseExpressionContext context ) : + FilterExpressionFunction( methodName, arguments, context ) { public const string Name = "count"; - // ReSharper disable once StaticMemberInGenericType private static readonly MethodInfo CountMethod; - static JsonPathCountFunction() + static CountElementFunction() { CountMethod = typeof( Enumerable ) .GetMethods( BindingFlags.Static | BindingFlags.Public ) @@ -18,10 +19,10 @@ static JsonPathCountFunction() m.Name == "Count" && m.GetParameters().Length == 1 && m.GetParameters()[0].ParameterType.GetGenericTypeDefinition() == typeof( IEnumerable<> ) ) - .MakeGenericMethod( typeof( TType ) ); + .MakeGenericMethod( typeof( JsonElement ) ); } - public override Expression GetExpression( string methodName, IList arguments, ParseExpressionContext context ) + public override Expression GetExpression( string methodName, IList arguments, ParseExpressionContext context ) { if ( arguments.Count != 1 ) { @@ -31,10 +32,11 @@ public override Expression GetExpression( string methodName, IList argum var queryExp = Expression.Constant( arguments[0] ); return Expression.Convert( Expression.Call( - CountMethod, - Expression.Call( JsonPathHelper.SelectMethod, - context.Current, - context.Root, - queryExp ) ), typeof( float ) ); + CountMethod, + Expression.Call( FilterElementHelper.SelectElementsMethod, + context.Current, + context.Root, + queryExp ) ) + , typeof( float ) ); } } diff --git a/src/Hyperbee.Json/Evaluators/Parser/Functions/JsonPathElementFunction.cs b/src/Hyperbee.Json/Evaluators/Parser/Element/FilterElementFunction.cs similarity index 51% rename from src/Hyperbee.Json/Evaluators/Parser/Functions/JsonPathElementFunction.cs rename to src/Hyperbee.Json/Evaluators/Parser/Element/FilterElementFunction.cs index 4d928d38..1218d8d7 100644 --- a/src/Hyperbee.Json/Evaluators/Parser/Functions/JsonPathElementFunction.cs +++ b/src/Hyperbee.Json/Evaluators/Parser/Element/FilterElementFunction.cs @@ -1,14 +1,14 @@ using System.Linq.Expressions; -namespace Hyperbee.Json.Evaluators.Parser.Functions; +namespace Hyperbee.Json.Evaluators.Parser.Element; -public class JsonPathElementFunction( ParseExpressionContext context ) : ParserFunction +public class FilterElementFunction( ParseExpressionContext context ) : FilterFunction { protected override Expression Evaluate( ReadOnlySpan data, ReadOnlySpan item, ref int start, ref int from ) { var queryExp = Expression.Constant( item.ToString() ); // Create a call expression for the extension method - return Expression.Call( JsonPathHelper.GetFirstElementValueMethod, context.Current, context.Root, queryExp ); + return Expression.Call( FilterElementHelper.SelectFirstElementValueMethod, context.Current, context.Root, queryExp ); } } diff --git a/src/Hyperbee.Json/Evaluators/Parser/Element/FilterElementHelper.cs b/src/Hyperbee.Json/Evaluators/Parser/Element/FilterElementHelper.cs new file mode 100644 index 00000000..6084b7c0 --- /dev/null +++ b/src/Hyperbee.Json/Evaluators/Parser/Element/FilterElementHelper.cs @@ -0,0 +1,61 @@ +using System.Reflection; +using System.Text.Json; + +namespace Hyperbee.Json.Evaluators.Parser.Element; + +public static class FilterElementHelper +{ + public static readonly MethodInfo SelectFirstElementValueMethod; + public static readonly MethodInfo SelectFirstMethod; + + public static readonly MethodInfo SelectElementsMethod; + + static FilterElementHelper() + { + var thisType = typeof( FilterElementHelper ); + + SelectFirstElementValueMethod = thisType.GetMethod( nameof( SelectFirstElementValue ), [typeof( JsonElement ), typeof( JsonElement ), typeof( string )] ); + SelectFirstMethod = thisType.GetMethod( nameof( SelectFirst ), [typeof( JsonElement ), typeof( JsonElement ), typeof( string )] ); + SelectElementsMethod = thisType.GetMethod( nameof( SelectElements ), [typeof( JsonElement ), typeof( JsonElement ), typeof( string )] ); + } + + private static bool IsNotEmpty( JsonElement element ) + { + return element.ValueKind switch + { + JsonValueKind.Array => element.EnumerateArray().Any(), + JsonValueKind.Object => element.EnumerateObject().Any(), + _ => false + }; + } + + public static object SelectFirstElementValue( JsonElement current, JsonElement root, string query ) + { + var element = SelectFirst( current, root, query ); + + return element.ValueKind switch + { + JsonValueKind.Number => element.GetSingle(), + JsonValueKind.String => element.GetString(), + JsonValueKind.Object => IsNotEmpty( element ), + JsonValueKind.Array => IsNotEmpty( element ), + JsonValueKind.True => true, + JsonValueKind.False => false, + JsonValueKind.Null => false, + JsonValueKind.Undefined => false, + _ => false + }; + } + + public static JsonElement SelectFirst( JsonElement current, JsonElement root, string query ) + { + return SelectElements( current, root, query ) + .FirstOrDefault(); + } + + public static IEnumerable SelectElements( JsonElement current, JsonElement root, string query ) + { + return JsonPath + .Select( current, root, query ); + } +} diff --git a/src/Hyperbee.Json/Evaluators/Parser/Element/JsonElementTypeDescriptor.cs b/src/Hyperbee.Json/Evaluators/Parser/Element/JsonElementTypeDescriptor.cs new file mode 100644 index 00000000..21f5c1aa --- /dev/null +++ b/src/Hyperbee.Json/Evaluators/Parser/Element/JsonElementTypeDescriptor.cs @@ -0,0 +1,27 @@ +namespace Hyperbee.Json.Evaluators.Parser.Element; + +public class JsonElementTypeDescriptor : IJsonTypeDescriptor +{ + public Dictionary Functions { get; init; } + + public IJsonValueAccessor GetAccessor() => + new JsonElementValueAccessor() as IJsonValueAccessor; + + public IJsonPathFilterEvaluator GetFilterEvaluator() => + new JsonPathFilterEvaluator( this ); + + public FilterFunction GetFilterFunction( ParseExpressionContext context ) => + new FilterElementFunction( context ); + + public JsonElementTypeDescriptor() + { + Functions = new Dictionary( + [ + new KeyValuePair( CountElementFunction.Name, ( name, arguments, context ) => new CountElementFunction( name, arguments, context ) ), + new KeyValuePair( LengthElementFunction.Name, ( name, arguments, context ) => new LengthElementFunction( name, arguments, context ) ), + new KeyValuePair( MatchElementFunction.Name, ( name, arguments, context ) => new MatchElementFunction( name, arguments, context ) ), + new KeyValuePair( SearchElementFunction.Name, ( name, arguments, context ) => new SearchElementFunction( name, arguments, context ) ), + new KeyValuePair( ValueElementFunction.Name, ( name, arguments, context ) => new ValueElementFunction( name, arguments, context ) ), + ] ); + } +} diff --git a/src/Hyperbee.Json/JsonPathElementVisitor.cs b/src/Hyperbee.Json/Evaluators/Parser/Element/JsonElementValueAccessor.cs similarity index 82% rename from src/Hyperbee.Json/JsonPathElementVisitor.cs rename to src/Hyperbee.Json/Evaluators/Parser/Element/JsonElementValueAccessor.cs index 8e1eaa8b..b303b9c7 100644 --- a/src/Hyperbee.Json/JsonPathElementVisitor.cs +++ b/src/Hyperbee.Json/Evaluators/Parser/Element/JsonElementValueAccessor.cs @@ -1,13 +1,12 @@ using System.Globalization; using System.Runtime.CompilerServices; using System.Text.Json; -using Hyperbee.Json.Extensions; -namespace Hyperbee.Json; +namespace Hyperbee.Json.Evaluators.Parser.Element; -public class JsonPathElementVisitor : JsonPathVisitorBase +public class JsonElementValueAccessor : IJsonValueAccessor { - internal override IEnumerable<(JsonElement, string)> EnumerateChildValues( JsonElement value ) + public IEnumerable<(JsonElement, string)> EnumerateChildValues( JsonElement value ) { switch ( value.ValueKind ) { @@ -50,19 +49,19 @@ public class JsonPathElementVisitor : JsonPathVisitorBase } [MethodImpl( MethodImplOptions.AggressiveInlining )] - internal override JsonElement GetElementAt( JsonElement value, int index ) + public JsonElement GetElementAt( JsonElement value, int index ) { return value[index]; } [MethodImpl( MethodImplOptions.AggressiveInlining )] - internal override bool IsObjectOrArray( JsonElement value ) + public bool IsObjectOrArray( JsonElement value ) { return value.ValueKind is JsonValueKind.Array or JsonValueKind.Object; } [MethodImpl( MethodImplOptions.AggressiveInlining )] - internal override bool IsArray( JsonElement value, out int length ) + public bool IsArray( JsonElement value, out int length ) { if ( value.ValueKind == JsonValueKind.Array ) { @@ -75,12 +74,12 @@ internal override bool IsArray( JsonElement value, out int length ) } [MethodImpl( MethodImplOptions.AggressiveInlining )] - internal override bool IsObject( JsonElement value ) + public bool IsObject( JsonElement value ) { return value.ValueKind is JsonValueKind.Object; } - internal override bool TryGetChildValue( in JsonElement value, ReadOnlySpan childKey, out JsonElement childValue ) + public bool TryGetChildValue( in JsonElement value, ReadOnlySpan childKey, out JsonElement childValue ) { static int? TryParseInt( ReadOnlySpan numberString ) { diff --git a/src/Hyperbee.Json/Evaluators/Parser/Element/LengthElementFunction.cs b/src/Hyperbee.Json/Evaluators/Parser/Element/LengthElementFunction.cs new file mode 100644 index 00000000..bc6f8aef --- /dev/null +++ b/src/Hyperbee.Json/Evaluators/Parser/Element/LengthElementFunction.cs @@ -0,0 +1,45 @@ +using System.Linq.Expressions; +using System.Reflection; +using System.Text.Json; + +namespace Hyperbee.Json.Evaluators.Parser.Element; + +public class LengthElementFunction( string methodName, IList arguments, ParseExpressionContext context ) : FilterExpressionFunction( methodName, arguments, context ) +{ + public const string Name = "length"; + + private static readonly MethodInfo LengthMethod; + + static LengthElementFunction() + { + LengthMethod = typeof( LengthElementFunction ).GetMethod( nameof( Length ), [typeof( JsonElement )] ); + } + + public override Expression GetExpression( string methodName, IList arguments, ParseExpressionContext context ) + { + if ( arguments.Count != 1 ) + { + return Expression.Throw( Expression.Constant( new ArgumentException( $"{Name} function has invalid parameter count." ) ) ); + } + + var queryExp = Expression.Constant( arguments[0] ); + + return Expression.Call( + LengthMethod, + Expression.Call( FilterElementHelper.SelectFirstMethod, + context.Current, + context.Root, + queryExp ) ); + } + + public static float Length( JsonElement element ) + { + return element.ValueKind switch + { + JsonValueKind.String => element.GetString()?.Length ?? 0, + JsonValueKind.Array => element.GetArrayLength(), + JsonValueKind.Object => element.EnumerateObject().Count(), + _ => 0 + }; + } +} diff --git a/src/Hyperbee.Json/Evaluators/Parser/Element/MatchElementFunction.cs b/src/Hyperbee.Json/Evaluators/Parser/Element/MatchElementFunction.cs new file mode 100644 index 00000000..810cae6f --- /dev/null +++ b/src/Hyperbee.Json/Evaluators/Parser/Element/MatchElementFunction.cs @@ -0,0 +1,45 @@ +using System.Linq.Expressions; +using System.Reflection; +using System.Text.Json; +using System.Text.RegularExpressions; + +namespace Hyperbee.Json.Evaluators.Parser.Element; + +public class MatchElementFunction( string methodName, IList arguments, ParseExpressionContext context ) : FilterExpressionFunction( methodName, arguments, context ) +{ + public const string Name = "match"; + + private static readonly MethodInfo MatchMethod; + + static MatchElementFunction() + { + MatchMethod = typeof( MatchElementFunction ).GetMethod( nameof( Match ), [typeof( JsonElement ), typeof( string )] ); + } + + public override Expression GetExpression( string methodName, IList arguments, ParseExpressionContext context ) + { + if ( arguments.Count != 2 ) + { + 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] ); + + return Expression.Call( + MatchMethod, + Expression.Call( FilterElementHelper.SelectFirstMethod, + context.Current, + context.Root, + queryExp ) + , regex ); + } + + public static bool Match( JsonElement element, string regex ) + { + var regexPattern = new Regex( regex.Trim( '\"', '\'' ) ); + var value = $"^{element.GetString()}$"; + + return regexPattern.IsMatch( value ); + } +} diff --git a/src/Hyperbee.Json/Evaluators/Parser/Element/SearchElementFunction.cs b/src/Hyperbee.Json/Evaluators/Parser/Element/SearchElementFunction.cs new file mode 100644 index 00000000..2993e9a0 --- /dev/null +++ b/src/Hyperbee.Json/Evaluators/Parser/Element/SearchElementFunction.cs @@ -0,0 +1,45 @@ +using System.Linq.Expressions; +using System.Reflection; +using System.Text.Json; +using System.Text.RegularExpressions; + +namespace Hyperbee.Json.Evaluators.Parser.Element; + +public class SearchElementFunction( string methodName, IList arguments, ParseExpressionContext context ) : FilterExpressionFunction( methodName, arguments, context ) +{ + public const string Name = "search"; + + private static readonly MethodInfo SearchMethod; + + static SearchElementFunction() + { + SearchMethod = typeof( SearchElementFunction ).GetMethod( nameof( Search ), [typeof( JsonElement ), typeof( string )] ); + } + + public override Expression GetExpression( string methodName, IList arguments, ParseExpressionContext context ) + { + if ( arguments.Count != 2 ) + { + 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] ); + + return Expression.Call( + SearchMethod, + Expression.Call( FilterElementHelper.SelectFirstMethod, + context.Current, + context.Root, + queryExp ) + , regex ); + } + + public static bool Search( JsonElement element, string regex ) + { + var regexPattern = new Regex( regex.Trim( '\"', '\'' ) ); + var value = element.GetString(); + + return value != null && regexPattern.IsMatch( value ); + } +} diff --git a/src/Hyperbee.Json/Evaluators/Parser/Functions/JsonPathValueFunction.cs b/src/Hyperbee.Json/Evaluators/Parser/Element/ValueElementFunction.cs similarity index 58% rename from src/Hyperbee.Json/Evaluators/Parser/Functions/JsonPathValueFunction.cs rename to src/Hyperbee.Json/Evaluators/Parser/Element/ValueElementFunction.cs index 20eca5de..c9533927 100644 --- a/src/Hyperbee.Json/Evaluators/Parser/Functions/JsonPathValueFunction.cs +++ b/src/Hyperbee.Json/Evaluators/Parser/Element/ValueElementFunction.cs @@ -1,12 +1,12 @@ using System.Linq.Expressions; -namespace Hyperbee.Json.Evaluators.Parser.Functions; +namespace Hyperbee.Json.Evaluators.Parser.Element; -public class JsonPathValueFunction( string methodName, IList arguments, ParseExpressionContext context ) : ParserExpressionFunction( methodName, arguments, context ) +public class ValueElementFunction( string methodName, IList arguments, ParseExpressionContext context ) : FilterExpressionFunction( methodName, arguments, context ) { public const string Name = "value"; - public override Expression GetExpression( string methodName, IList arguments, ParseExpressionContext context ) + public override Expression GetExpression( string methodName, IList arguments, ParseExpressionContext context ) { if ( arguments.Count != 1 ) { @@ -16,7 +16,7 @@ public override Expression GetExpression( string methodName, IList argum var queryExp = Expression.Constant( arguments[0] ); return Expression.Call( - JsonPathHelper.GetFirstElementValueMethod, + FilterElementHelper.SelectFirstElementValueMethod, context.Current, context.Root, queryExp ); diff --git a/src/Hyperbee.Json/Evaluators/Parser/Functions/ParserExpressionFunction.cs b/src/Hyperbee.Json/Evaluators/Parser/FilterExpressionFunction.cs similarity index 51% rename from src/Hyperbee.Json/Evaluators/Parser/Functions/ParserExpressionFunction.cs rename to src/Hyperbee.Json/Evaluators/Parser/FilterExpressionFunction.cs index 494e886a..7865c5fe 100644 --- a/src/Hyperbee.Json/Evaluators/Parser/Functions/ParserExpressionFunction.cs +++ b/src/Hyperbee.Json/Evaluators/Parser/FilterExpressionFunction.cs @@ -1,10 +1,14 @@ using System.Linq.Expressions; -namespace Hyperbee.Json.Evaluators.Parser.Functions; +namespace Hyperbee.Json.Evaluators.Parser; -public abstract class ParserExpressionFunction( string methodName, IList arguments, ParseExpressionContext context ) : ParserFunction +public abstract class FilterExpressionFunction( + string methodName, + IList arguments, + ParseExpressionContext context +) : FilterFunction { - public abstract Expression GetExpression( string methodName, IList arguments, ParseExpressionContext context ); + public abstract Expression GetExpression( string methodName, IList arguments, ParseExpressionContext context ); protected override Expression Evaluate( ReadOnlySpan data, ReadOnlySpan item, ref int start, ref int from ) { diff --git a/src/Hyperbee.Json/Evaluators/Parser/FilterFunction.cs b/src/Hyperbee.Json/Evaluators/Parser/FilterFunction.cs new file mode 100644 index 00000000..72ae6d8a --- /dev/null +++ b/src/Hyperbee.Json/Evaluators/Parser/FilterFunction.cs @@ -0,0 +1,79 @@ +using System.Linq.Expressions; +using static Hyperbee.Json.Evaluators.Parser.JsonPathExpression; + +namespace Hyperbee.Json.Evaluators.Parser; + +public class FilterFunction +{ + private readonly FilterFunction _implementation; + + public FilterFunction() + { + _implementation = this; + } + + internal FilterFunction( ReadOnlySpan item, FilterTokenType? type, ParseExpressionContext context ) + { + if ( item.Length == 0 && type == FilterTokenType.OpenParen ) + { + // There is no function, just an expression in parentheses. + _implementation = new ParenFunction( context ); + return; + } + + switch ( item[0] ) + { + case '@': + _implementation = context.Descriptor.GetFilterFunction( context ); + return; + case '$': + // Current becomes root + _implementation = context.Descriptor.GetFilterFunction( context with { Current = context.Root } ); + return; + } + + if ( TryGetExpressionFunction( item, context, out _implementation ) ) + { + // Methods based on spec + return; + } + + // Function not found, will try to parse this as a literal value. + var literalFunction = new LiteralFunction(); + _implementation = literalFunction; + } + + public Expression GetExpression( ReadOnlySpan data, ReadOnlySpan item, ref int start, ref int from ) + { + return _implementation.Evaluate( data, item, ref start, ref from ); + } + + protected virtual Expression Evaluate( ReadOnlySpan data, ReadOnlySpan item, ref int start, ref int from ) + { + // The real implementation will be in the derived classes. + return Expression.Throw( Expression.Constant( new NotImplementedException() ) ); + } + + private static bool TryGetExpressionFunction( ReadOnlySpan item, ParseExpressionContext context, out FilterFunction function ) + { + var match = FilterTokenizerRegex.RegexFunction().Match( item.ToString() ); + + if ( match.Groups.Count != 3 ) + { + function = null; + return false; + } + + var method = match.Groups[1].Value; + var arguments = match.Groups[2].Value.Split( ',', options: StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries ); + + if ( context.Descriptor.Functions.TryGetValue( method.ToLowerInvariant(), out var creator ) ) + { + function = creator( method, arguments, context ); + return true; + } + + function = null; + return false; + } +} diff --git a/src/Hyperbee.Json/Evaluators/Parser/Functions/JsonPathFilterTokenizerRegex.cs b/src/Hyperbee.Json/Evaluators/Parser/FilterTokenizerRegex.cs similarity index 79% rename from src/Hyperbee.Json/Evaluators/Parser/Functions/JsonPathFilterTokenizerRegex.cs rename to src/Hyperbee.Json/Evaluators/Parser/FilterTokenizerRegex.cs index 5263b56e..506d6772 100644 --- a/src/Hyperbee.Json/Evaluators/Parser/Functions/JsonPathFilterTokenizerRegex.cs +++ b/src/Hyperbee.Json/Evaluators/Parser/FilterTokenizerRegex.cs @@ -1,8 +1,8 @@ using System.Text.RegularExpressions; -namespace Hyperbee.Json.Evaluators.Parser.Functions; +namespace Hyperbee.Json.Evaluators.Parser; -internal static partial class JsonPathFilterTokenizerRegex +internal static partial class FilterTokenizerRegex { [GeneratedRegex( @"([a-z][a-z0-9_]*)\s*\(\s*((?:[^,()]+(?:\s*,\s*)?)*)\s*\)?" )] internal static partial Regex RegexFunction(); diff --git a/src/Hyperbee.Json/Evaluators/Parser/FilterTruthyExpression.cs b/src/Hyperbee.Json/Evaluators/Parser/FilterTruthyExpression.cs new file mode 100644 index 00000000..020b1f7e --- /dev/null +++ b/src/Hyperbee.Json/Evaluators/Parser/FilterTruthyExpression.cs @@ -0,0 +1,34 @@ +using System.Linq.Expressions; +using System.Reflection; + +namespace Hyperbee.Json.Evaluators.Parser; + +public static class FilterTruthyExpression +{ + private static readonly MethodInfo IsTruthyMethod; + + static FilterTruthyExpression() + { + IsTruthyMethod = typeof( FilterTruthyExpression ).GetMethod( nameof( IsTruthy ), [typeof( object )] ); + } + + public static Expression IsTruthyExpression( Expression expression ) => + expression.Type == typeof( bool ) + ? expression + : Expression.Call( IsTruthyMethod, expression ); + + public static bool IsTruthy( object obj ) => !IsFalsy( obj ); + + public static bool IsFalsy( object obj ) + { + return obj switch + { + null => true, + bool boolValue => !boolValue, + string str => string.IsNullOrEmpty( str ) || str == "false", + Array array => array.Length == 0, + IConvertible convertible => !Convert.ToBoolean( convertible ), + _ => false + }; + } +} diff --git a/src/Hyperbee.Json/Evaluators/Parser/FunctionCreator.cs b/src/Hyperbee.Json/Evaluators/Parser/FunctionCreator.cs new file mode 100644 index 00000000..0cc8b740 --- /dev/null +++ b/src/Hyperbee.Json/Evaluators/Parser/FunctionCreator.cs @@ -0,0 +1,6 @@ +namespace Hyperbee.Json.Evaluators.Parser; + +public delegate FilterExpressionFunction FunctionCreator( + string methodName, + IList arguments, + ParseExpressionContext context = null ); diff --git a/src/Hyperbee.Json/Evaluators/Parser/Functions/JsonPathHelper.cs b/src/Hyperbee.Json/Evaluators/Parser/Functions/JsonPathHelper.cs deleted file mode 100644 index 53baefba..00000000 --- a/src/Hyperbee.Json/Evaluators/Parser/Functions/JsonPathHelper.cs +++ /dev/null @@ -1,131 +0,0 @@ -using System.Reflection; -using System.Text.Json; -using System.Text.Json.Nodes; -using Hyperbee.Json.Extensions; - -namespace Hyperbee.Json.Evaluators.Parser.Functions; - -public static class JsonPathHelper //BF: Is this the right name? JsonPathFilterHelper ? -{ - // ReSharper disable once StaticMemberInGenericType - public static readonly MethodInfo GetFirstElementValueMethod; - - // ReSharper disable once StaticMemberInGenericType - public static readonly MethodInfo GetFirstElementMethod; - - // ReSharper disable once StaticMemberInGenericType - public static readonly MethodInfo IsTruthyMethod; - - // ReSharper disable once StaticMemberInGenericType - public static readonly MethodInfo SelectMethod; - - static JsonPathHelper() - { - var thisType = typeof( JsonPathHelper ); - - 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 ) ); - } - - public static bool IsTruthy( object obj ) => !IsFalsy( obj ); - - public static bool IsFalsy( object obj ) - { - return obj switch - { - null => true, - bool boolValue => !boolValue, - string str => string.IsNullOrEmpty( str ) || str == "false", - Array array => array.Length == 0, - IConvertible convertible => !Convert.ToBoolean( convertible ), - _ => false - }; - } - - private static bool IsNotEmpty( JsonElement element ) - { - return element.ValueKind switch - { - JsonValueKind.Array => element.EnumerateArray().Any(), - JsonValueKind.Object => element.EnumerateObject().Any(), - _ => false - }; - } - - private static bool IsNotEmpty( JsonNode node ) - { - return node.GetValueKind() switch - { - JsonValueKind.Array => node.AsArray().Count != 0, - JsonValueKind.Object => node.AsObject().Count != 0, - _ => false - }; - } - - public static object GetFirstElementValue( JsonElement current, JsonElement root, string query ) - { - var first = GetFirstElement( current, root, query ); - - return first.ValueKind switch - { - JsonValueKind.Number => first.GetSingle(), - JsonValueKind.String => first.GetString(), - JsonValueKind.Object => IsNotEmpty( first ), - JsonValueKind.Array => IsNotEmpty( first ), - JsonValueKind.True => true, - JsonValueKind.False => false, - JsonValueKind.Null => false, - JsonValueKind.Undefined => false, - _ => false - }; - } - - public static object GetFirstElementValue( JsonNode current, JsonNode root, string query ) - { - var first = GetFirstElement( current, root, query ); - - return first?.GetValueKind() switch - { - JsonValueKind.Number => first.GetNumber(), - JsonValueKind.String => first.GetValue(), - JsonValueKind.Object => IsNotEmpty( first ), - JsonValueKind.Array => IsNotEmpty( first ), - JsonValueKind.True => true, - JsonValueKind.False => false, - JsonValueKind.Null => false, - JsonValueKind.Undefined => false, - _ => false - }; - } - - //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() - .Select( current, root, query ) - .FirstOrDefault(); - } - - public static JsonNode GetFirstElement( JsonNode current, JsonNode root, string query ) - { - return new Nodes.JsonPathNode() - .Select( current, root, query ) - .FirstOrDefault(); - } - - public static IEnumerable Select( JsonElement current, JsonElement root, string query ) - { - return new JsonPath() - .Select( current, root, query ); - } - - public static IEnumerable Select( JsonNode current, JsonNode root, string query ) - { - return new Nodes.JsonPathNode() - .Select( current, root, query ); - } -} diff --git a/src/Hyperbee.Json/Evaluators/Parser/Functions/JsonPathMatchFunction.cs b/src/Hyperbee.Json/Evaluators/Parser/Functions/JsonPathMatchFunction.cs deleted file mode 100644 index 86474a11..00000000 --- a/src/Hyperbee.Json/Evaluators/Parser/Functions/JsonPathMatchFunction.cs +++ /dev/null @@ -1,55 +0,0 @@ -using System.Linq.Expressions; -using System.Reflection; -using System.Text.Json; -using System.Text.Json.Nodes; -using System.Text.RegularExpressions; - -namespace Hyperbee.Json.Evaluators.Parser.Functions; - -public class JsonPathMatchFunction( string methodName, IList arguments, ParseExpressionContext context ) : ParserExpressionFunction( methodName, arguments, context ) -{ - public const string Name = "match"; - - // ReSharper disable once StaticMemberInGenericType - private static readonly MethodInfo MatchMethod; - - static JsonPathMatchFunction() - { - MatchMethod = typeof( JsonPathMatchFunction ).GetMethod( nameof( Match ), [typeof( TType ), typeof( string )] ); - } - - public override Expression GetExpression( string methodName, IList arguments, ParseExpressionContext context ) - { - if ( arguments.Count != 2 ) - { - 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] ); - - return Expression.Call( - MatchMethod, - Expression.Call( JsonPathHelper.GetFirstElementMethod, - context.Current, - context.Root, - queryExp ) - , regex ); - } - - public static bool Match( JsonElement element, string regex ) - { - var regexPattern = new Regex( regex.Trim( '\"', '\'' ) ); - var value = element.GetString(); - - return value != null && regexPattern.IsMatch( value ); - } - - public static bool Match( JsonNode node, string regex ) - { - var regexPattern = new Regex( regex.Trim( '\"', '\'' ) ); - var value = node.GetValue(); - - return value != null && regexPattern.IsMatch( value ); - } -} diff --git a/src/Hyperbee.Json/Evaluators/Parser/Functions/ParserFunction.cs b/src/Hyperbee.Json/Evaluators/Parser/Functions/ParserFunction.cs deleted file mode 100644 index ba86abcd..00000000 --- a/src/Hyperbee.Json/Evaluators/Parser/Functions/ParserFunction.cs +++ /dev/null @@ -1,102 +0,0 @@ -using System.Collections.Concurrent; -using System.Linq.Expressions; -using static Hyperbee.Json.Evaluators.Parser.JsonPathExpression; - -namespace Hyperbee.Json.Evaluators.Parser.Functions; - -public class ParserFunction -{ - public delegate ParserExpressionFunction FunctionCreator( string methodName, IList arguments, ParseExpressionContext context = null ); - - private readonly ParserFunction _implementation; - private static readonly ConcurrentDictionary ExpressionFunctions; - - static ParserFunction() - { - ExpressionFunctions = new ConcurrentDictionary( - [ - new KeyValuePair( JsonPathCountFunction.Name, ( name, arguments, context ) => new JsonPathCountFunction( name, arguments, context ) ), - new KeyValuePair( JsonPathLengthFunction.Name, ( name, arguments, context ) => new JsonPathLengthFunction( name, arguments, context ) ), - new KeyValuePair( JsonPathMatchFunction.Name, ( name, arguments, context ) => new JsonPathMatchFunction( name, arguments, context ) ), - new KeyValuePair( JsonPathSearchFunction.Name, ( name, arguments, context ) => new JsonPathSearchFunction( name, arguments, context ) ), - new KeyValuePair( JsonPathValueFunction.Name, ( name, arguments, context ) => new JsonPathValueFunction( name, arguments, context ) ), - ] ); - } - - public ParserFunction() - { - _implementation = this; - } - - internal ParserFunction( ReadOnlySpan item, FilterTokenType? type, ParseExpressionContext context ) - { - if ( item.Length == 0 && type == FilterTokenType.OpenParen ) - { - // There is no function, just an expression in parentheses. - _implementation = new ParenFunction( context ); - return; - } - - var currentPath = item[0] == '@'; - var rootPath = item[0] == '$'; - - if ( item.Length > 0 && (currentPath || rootPath) ) - { - // There is a JsonPath sub query. - _implementation = rootPath - ? new JsonPathElementFunction( context with { Current = context.Root } ) // Current becomes root - : new JsonPathElementFunction( context ); - - return; - } - - if ( TryGetExpressionFunction( item, context, out _implementation ) ) - { - // Methods based on spec - return; - } - - // Function not found, will try to parse this as a literal value. - var literalFunction = new LiteralFunction(); - _implementation = literalFunction; - } - - public Expression GetExpression( ReadOnlySpan data, ReadOnlySpan item, ref int start, ref int from ) - { - return _implementation.Evaluate( data, item, ref start, ref from ); - } - - public static void AddFunction( string name, FunctionCreator creator ) - { - ExpressionFunctions[name] = creator; - } - - protected virtual Expression Evaluate( ReadOnlySpan data, ReadOnlySpan item, ref int start, ref int from ) - { - // The real implementation will be in the derived classes. - return Expression.Throw( Expression.Constant( new NotImplementedException() ) ); - } - - private static bool TryGetExpressionFunction( ReadOnlySpan item, ParseExpressionContext context, out ParserFunction function ) - { - var match = JsonPathFilterTokenizerRegex.RegexFunction().Match( item.ToString() ); - - if ( match.Groups.Count != 3 ) - { - function = null; - return false; - } - - var method = match.Groups[1].Value; - var arguments = match.Groups[2].Value.Split( ',', options: StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries ); - - if ( ExpressionFunctions.TryGetValue( method.ToLowerInvariant(), out var creator ) ) - { - function = creator( method, arguments, context ); - return true; - } - - function = null; - return false; - } -} diff --git a/src/Hyperbee.Json/Evaluators/Parser/JsonPathExpression.cs b/src/Hyperbee.Json/Evaluators/Parser/JsonPathExpression.cs index 11d5b347..5a4908db 100644 --- a/src/Hyperbee.Json/Evaluators/Parser/JsonPathExpression.cs +++ b/src/Hyperbee.Json/Evaluators/Parser/JsonPathExpression.cs @@ -1,6 +1,5 @@ using System.Linq.Expressions; using System.Reflection; -using Hyperbee.Json.Evaluators.Parser.Functions; namespace Hyperbee.Json.Evaluators.Parser; // Based off Split-and-Merge Expression Parser @@ -16,30 +15,28 @@ public class JsonPathExpression private static readonly MethodInfo ObjectEquals = typeof( object ).GetMethod( "Equals", [typeof( object ), typeof( object )] ); - public static Func Compile( ReadOnlySpan filter ) + public static Func Compile( ReadOnlySpan filter, IJsonTypeDescriptor typeDescriptor ) { - var currentParam = Expression.Parameter( typeof( TType ) ); - var rootParam = Expression.Parameter( typeof( TType ) ); - var expressionContext = new ParseExpressionContext( currentParam, rootParam ); + var currentParam = Expression.Parameter( typeof( TElement ) ); + var rootParam = Expression.Parameter( typeof( TElement ) ); + var expressionContext = new ParseExpressionContext( currentParam, rootParam, typeDescriptor ); var expression = Parse( filter, expressionContext ); return Expression - .Lambda>( expression, currentParam, rootParam ) + .Lambda>( expression, currentParam, rootParam ) .Compile(); } - public static Expression Parse( ReadOnlySpan filter, ParseExpressionContext context ) + public static Expression Parse( ReadOnlySpan filter, ParseExpressionContext context ) { var start = 0; var from = 0; var expression = Parse( filter, ref start, ref from, EndLine, context ); - return expression.Type == typeof( bool ) - ? expression - : Expression.Call( JsonPathHelper.IsTruthyMethod!, expression ); + return FilterTruthyExpression.IsTruthyExpression( expression ); } - internal static Expression Parse( ReadOnlySpan filter, ref int start, ref int from, char to = EndLine, ParseExpressionContext context = null ) + internal static Expression Parse( ReadOnlySpan filter, ref int start, ref int from, char to = EndLine, ParseExpressionContext context = null ) { if ( from >= filter.Length || filter[from] == to ) { @@ -78,7 +75,7 @@ internal static Expression Parse( ReadOnlySpan filter, ref int star start = from; // `GetExpression` may call recursively call `Parse` for nested expressions - var func = new ParserFunction( currentPath, type, context ); + var func = new FilterFunction( currentPath, type, context ); var expression = func.GetExpression( filter, currentPath, ref start, ref from ); var filterType = ValidType( type ) diff --git a/src/Hyperbee.Json/Evaluators/Parser/Functions/LiteralFunction.cs b/src/Hyperbee.Json/Evaluators/Parser/LiteralFunction.cs similarity index 80% rename from src/Hyperbee.Json/Evaluators/Parser/Functions/LiteralFunction.cs rename to src/Hyperbee.Json/Evaluators/Parser/LiteralFunction.cs index 5583a3f4..3f496dd6 100644 --- a/src/Hyperbee.Json/Evaluators/Parser/Functions/LiteralFunction.cs +++ b/src/Hyperbee.Json/Evaluators/Parser/LiteralFunction.cs @@ -1,16 +1,16 @@ using System.Linq.Expressions; -namespace Hyperbee.Json.Evaluators.Parser.Functions; +namespace Hyperbee.Json.Evaluators.Parser; -public class LiteralFunction : ParserFunction +public class LiteralFunction : FilterFunction { protected override Expression Evaluate( ReadOnlySpan data, ReadOnlySpan item, ref int start, ref int from ) { // strings double or single - if ( JsonPathFilterTokenizerRegex.RegexQuotedDouble().IsMatch( item ) ) + if ( FilterTokenizerRegex.RegexQuotedDouble().IsMatch( item ) ) return Expression.Constant( TrimQuotes( item ).ToString() ); - if ( JsonPathFilterTokenizerRegex.RegexQuoted().IsMatch( item ) ) + if ( FilterTokenizerRegex.RegexQuoted().IsMatch( item ) ) return Expression.Constant( TrimQuotes( item ).ToString() ); // known literals (true, false, null) @@ -30,7 +30,7 @@ static ReadOnlySpan TrimQuotes( ReadOnlySpan input ) if ( input.Length < 2 ) return input; - if ( (input[0] == '\'' && input[^1] == '\'') || (input[0] == '\"' && input[^1] == '\"') ) + if ( input[0] == '\'' && input[^1] == '\'' || input[0] == '\"' && input[^1] == '\"' ) return input[1..^1]; return input; diff --git a/src/Hyperbee.Json/Evaluators/Parser/Node/CountNodeFunction.cs b/src/Hyperbee.Json/Evaluators/Parser/Node/CountNodeFunction.cs new file mode 100644 index 00000000..111af350 --- /dev/null +++ b/src/Hyperbee.Json/Evaluators/Parser/Node/CountNodeFunction.cs @@ -0,0 +1,42 @@ +using System.Linq.Expressions; +using System.Reflection; +using System.Text.Json.Nodes; + +namespace Hyperbee.Json.Evaluators.Parser.Node; + +public class CountNodeFunction( string methodName, IList arguments, ParseExpressionContext context ) : + FilterExpressionFunction( methodName, arguments, context ) +{ + public const string Name = "count"; + + private static readonly MethodInfo CountMethod; + + static CountNodeFunction() + { + CountMethod = typeof( Enumerable ) + .GetMethods( BindingFlags.Static | BindingFlags.Public ) + .First( m => + m.Name == "Count" && + m.GetParameters().Length == 1 && + m.GetParameters()[0].ParameterType.GetGenericTypeDefinition() == typeof( IEnumerable<> ) ) + .MakeGenericMethod( typeof( JsonNode ) ); + } + + public override Expression GetExpression( string methodName, IList arguments, ParseExpressionContext context ) + { + if ( arguments.Count != 1 ) + { + return Expression.Throw( Expression.Constant( new Exception( $"Invalid use of {Name} function" ) ) ); + } + + var queryExp = Expression.Constant( arguments[0] ); + + return Expression.Convert( Expression.Call( + CountMethod, + Expression.Call( FilterNodeHelper.SelectElementsMethod, + context.Current, + context.Root, + queryExp ) ) + , typeof( float ) ); + } +} diff --git a/src/Hyperbee.Json/Evaluators/Parser/Node/FilterNodeFunction.cs b/src/Hyperbee.Json/Evaluators/Parser/Node/FilterNodeFunction.cs new file mode 100644 index 00000000..ca2a3b72 --- /dev/null +++ b/src/Hyperbee.Json/Evaluators/Parser/Node/FilterNodeFunction.cs @@ -0,0 +1,14 @@ +using System.Linq.Expressions; + +namespace Hyperbee.Json.Evaluators.Parser.Node; + +public class FilterNodeFunction( ParseExpressionContext context ) : FilterFunction +{ + protected override Expression Evaluate( ReadOnlySpan data, ReadOnlySpan item, ref int start, ref int from ) + { + var queryExp = Expression.Constant( item.ToString() ); + + // Create a call expression for the extension method + return Expression.Call( FilterNodeHelper.SelectFirstElementValueMethod, context.Current, context.Root, queryExp ); + } +} diff --git a/src/Hyperbee.Json/Evaluators/Parser/Node/FilterNodeHelper.cs b/src/Hyperbee.Json/Evaluators/Parser/Node/FilterNodeHelper.cs new file mode 100644 index 00000000..474e1a29 --- /dev/null +++ b/src/Hyperbee.Json/Evaluators/Parser/Node/FilterNodeHelper.cs @@ -0,0 +1,63 @@ +using System.Reflection; +using System.Text.Json; +using System.Text.Json.Nodes; +using Hyperbee.Json.Extensions; + +namespace Hyperbee.Json.Evaluators.Parser.Node; + +public static class FilterNodeHelper +{ + public static readonly MethodInfo SelectFirstElementValueMethod; + public static readonly MethodInfo SelectFirstMethod; + public static readonly MethodInfo SelectElementsMethod; + + static FilterNodeHelper() + { + var thisType = typeof( FilterNodeHelper ); + + SelectFirstElementValueMethod = thisType.GetMethod( nameof( SelectFirstElementValue ), [typeof( JsonNode ), typeof( JsonNode ), typeof( string )] ); + SelectFirstMethod = thisType.GetMethod( nameof( SelectFirst ), [typeof( JsonNode ), typeof( JsonNode ), typeof( string )] ); + SelectElementsMethod = thisType.GetMethod( nameof( SelectElements ), [typeof( JsonNode ), typeof( JsonNode ), typeof( string )] ); + } + + private static bool IsNotEmpty( JsonNode node ) + { + return node.GetValueKind() switch + { + JsonValueKind.Array => node.AsArray().Count != 0, + JsonValueKind.Object => node.AsObject().Count != 0, + _ => false + }; + } + + public static object SelectFirstElementValue( JsonNode current, JsonNode root, string query ) + { + var node = SelectFirst( current, root, query ); + + return node?.GetValueKind() switch + { + JsonValueKind.Number => node.GetNumber(), + JsonValueKind.String => node.GetValue(), + JsonValueKind.Object => IsNotEmpty( node ), + JsonValueKind.Array => IsNotEmpty( node ), + JsonValueKind.True => true, + JsonValueKind.False => false, + JsonValueKind.Null => false, + JsonValueKind.Undefined => false, + _ => false + }; + } + + public static JsonNode SelectFirst( JsonNode current, JsonNode root, string query ) + { + return JsonPath + .Select( current, root, query ) + .FirstOrDefault(); + } + + public static IEnumerable SelectElements( JsonNode current, JsonNode root, string query ) + { + return JsonPath + .Select( current, root, query ); + } +} diff --git a/src/Hyperbee.Json/Evaluators/Parser/Node/JsonNodeTypeDescriptor.cs b/src/Hyperbee.Json/Evaluators/Parser/Node/JsonNodeTypeDescriptor.cs new file mode 100644 index 00000000..f2b5b6d2 --- /dev/null +++ b/src/Hyperbee.Json/Evaluators/Parser/Node/JsonNodeTypeDescriptor.cs @@ -0,0 +1,27 @@ +namespace Hyperbee.Json.Evaluators.Parser.Node; + +public class JsonNodeTypeDescriptor : IJsonTypeDescriptor +{ + public Dictionary Functions { get; } + + public IJsonValueAccessor GetAccessor() => + new JsonNodeValueAccessor() as IJsonValueAccessor; + + public IJsonPathFilterEvaluator GetFilterEvaluator() => + new JsonPathFilterEvaluator( this ); + + public FilterFunction GetFilterFunction( ParseExpressionContext context ) => + new FilterNodeFunction( context ); + + public JsonNodeTypeDescriptor() + { + Functions = new Dictionary( + [ + new KeyValuePair( CountNodeFunction.Name, ( name, arguments, context ) => new CountNodeFunction( name, arguments, context ) ), + new KeyValuePair( LengthNodeFunction.Name, ( name, arguments, context ) => new LengthNodeFunction( name, arguments, context ) ), + new KeyValuePair( MatchNodeFunction.Name, ( name, arguments, context ) => new MatchNodeFunction( name, arguments, context ) ), + new KeyValuePair( SearchNodeFunction.Name, ( name, arguments, context ) => new SearchNodeFunction( name, arguments, context ) ), + new KeyValuePair( ValueNodeFunction.Name, ( name, arguments, context ) => new ValueNodeFunction( name, arguments, context ) ), + ] ); + } +} diff --git a/src/Hyperbee.Json/Nodes/JsonPathNodeVisitor.cs b/src/Hyperbee.Json/Evaluators/Parser/Node/JsonNodeValueAccessor.cs similarity index 84% rename from src/Hyperbee.Json/Nodes/JsonPathNodeVisitor.cs rename to src/Hyperbee.Json/Evaluators/Parser/Node/JsonNodeValueAccessor.cs index 2fcd14ac..875b56f6 100644 --- a/src/Hyperbee.Json/Nodes/JsonPathNodeVisitor.cs +++ b/src/Hyperbee.Json/Evaluators/Parser/Node/JsonNodeValueAccessor.cs @@ -2,11 +2,11 @@ using System.Runtime.CompilerServices; using System.Text.Json.Nodes; -namespace Hyperbee.Json.Nodes; +namespace Hyperbee.Json.Evaluators.Parser.Node; -internal class JsonPathNodeVisitor : JsonPathVisitorBase +internal class JsonNodeValueAccessor : IJsonValueAccessor { - internal override IEnumerable<(JsonNode, string)> EnumerateChildValues( JsonNode value ) + public IEnumerable<(JsonNode, string)> EnumerateChildValues( JsonNode value ) { switch ( value ) { @@ -45,19 +45,19 @@ internal class JsonPathNodeVisitor : JsonPathVisitorBase } [MethodImpl( MethodImplOptions.AggressiveInlining )] - internal override JsonNode GetElementAt( JsonNode value, int index ) + public JsonNode GetElementAt( JsonNode value, int index ) { return value[index]; } [MethodImpl( MethodImplOptions.AggressiveInlining )] - internal override bool IsObjectOrArray( JsonNode value ) + public bool IsObjectOrArray( JsonNode value ) { return value is JsonObject or JsonArray; } [MethodImpl( MethodImplOptions.AggressiveInlining )] - internal override bool IsArray( JsonNode value, out int length ) + public bool IsArray( JsonNode value, out int length ) { if ( value is JsonArray jsonArray ) { @@ -70,12 +70,12 @@ internal override bool IsArray( JsonNode value, out int length ) } [MethodImpl( MethodImplOptions.AggressiveInlining )] - internal override bool IsObject( JsonNode value ) + public bool IsObject( JsonNode value ) { return value is JsonObject; } - internal override bool TryGetChildValue( in JsonNode value, ReadOnlySpan childKey, out JsonNode childValue ) + public bool TryGetChildValue( in JsonNode value, ReadOnlySpan childKey, out JsonNode childValue ) { static int? TryParseInt( ReadOnlySpan numberString ) { @@ -90,6 +90,7 @@ internal override bool TryGetChildValue( in JsonNode value, ReadOnlySpan c { if ( valueObject.TryGetPropertyValue( childKey.ToString(), out childValue ) ) return true; + break; } case JsonArray valueArray: @@ -108,6 +109,7 @@ internal override bool TryGetChildValue( in JsonNode value, ReadOnlySpan c { if ( !IsPathOperator( childKey ) ) throw new ArgumentException( $"Invalid child type '{childKey.ToString()}'. Expected child to be Object, Array or a path selector.", nameof( value ) ); + break; } } diff --git a/src/Hyperbee.Json/Evaluators/Parser/Functions/JsonPathLengthFunction.cs b/src/Hyperbee.Json/Evaluators/Parser/Node/LengthNodeFunction.cs similarity index 52% rename from src/Hyperbee.Json/Evaluators/Parser/Functions/JsonPathLengthFunction.cs rename to src/Hyperbee.Json/Evaluators/Parser/Node/LengthNodeFunction.cs index f8fa1f36..27860b4b 100644 --- a/src/Hyperbee.Json/Evaluators/Parser/Functions/JsonPathLengthFunction.cs +++ b/src/Hyperbee.Json/Evaluators/Parser/Node/LengthNodeFunction.cs @@ -3,21 +3,20 @@ using System.Text.Json; using System.Text.Json.Nodes; -namespace Hyperbee.Json.Evaluators.Parser.Functions; +namespace Hyperbee.Json.Evaluators.Parser.Node; -public class JsonPathLengthFunction( string methodName, IList arguments, ParseExpressionContext context ) : ParserExpressionFunction( methodName, arguments, context ) +public class LengthNodeFunction( string methodName, IList arguments, ParseExpressionContext context ) : FilterExpressionFunction( methodName, arguments, context ) { public const string Name = "length"; - // ReSharper disable once StaticMemberInGenericType private static readonly MethodInfo LengthMethod; - static JsonPathLengthFunction() + static LengthNodeFunction() { - LengthMethod = typeof( JsonPathLengthFunction ).GetMethod( nameof( Length ), [typeof( TType )] ); + LengthMethod = typeof( LengthNodeFunction ).GetMethod( nameof( Length ), [typeof( JsonNode )] ); } - public override Expression GetExpression( string methodName, IList arguments, ParseExpressionContext context ) + public override Expression GetExpression( string methodName, IList arguments, ParseExpressionContext context ) { if ( arguments.Count != 1 ) { @@ -28,24 +27,12 @@ public override Expression GetExpression( string methodName, IList argum return Expression.Call( LengthMethod, - Expression.Call( JsonPathHelper.GetFirstElementMethod, + Expression.Call( FilterNodeHelper.SelectFirstMethod, context.Current, context.Root, queryExp ) ); } - - public static float Length( JsonElement element ) - { - return element.ValueKind switch - { - JsonValueKind.String => element.GetString()?.Length ?? 0, - JsonValueKind.Array => element.GetArrayLength(), - JsonValueKind.Object => element.EnumerateObject().Count(), - _ => 0 - }; - } - public static float Length( JsonNode node ) { return node.GetValueKind() switch diff --git a/src/Hyperbee.Json/Evaluators/Parser/Node/MatchNodeFunction.cs b/src/Hyperbee.Json/Evaluators/Parser/Node/MatchNodeFunction.cs new file mode 100644 index 00000000..a73fb332 --- /dev/null +++ b/src/Hyperbee.Json/Evaluators/Parser/Node/MatchNodeFunction.cs @@ -0,0 +1,45 @@ +using System.Linq.Expressions; +using System.Reflection; +using System.Text.Json.Nodes; +using System.Text.RegularExpressions; + +namespace Hyperbee.Json.Evaluators.Parser.Node; + +public class MatchNodeFunction( string methodName, IList arguments, ParseExpressionContext context ) : FilterExpressionFunction( methodName, arguments, context ) +{ + public const string Name = "match"; + + private static readonly MethodInfo MatchMethod; + + static MatchNodeFunction() + { + MatchMethod = typeof( MatchNodeFunction ).GetMethod( nameof( Match ), [typeof( JsonNode ), typeof( string )] ); + } + + public override Expression GetExpression( string methodName, IList arguments, ParseExpressionContext context ) + { + if ( arguments.Count != 2 ) + { + 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] ); + + return Expression.Call( + MatchMethod, + Expression.Call( FilterNodeHelper.SelectFirstMethod, + context.Current, + context.Root, + queryExp ) + , regex ); + } + + public static bool Match( JsonNode node, string regex ) + { + var regexPattern = new Regex( regex.Trim( '\"', '\'' ) ); + var value = $"^{node.GetValue()}$"; + + return regexPattern.IsMatch( value ); + } +} diff --git a/src/Hyperbee.Json/Evaluators/Parser/Functions/JsonPathSearchFunction.cs b/src/Hyperbee.Json/Evaluators/Parser/Node/SearchElementFunction.cs similarity index 50% rename from src/Hyperbee.Json/Evaluators/Parser/Functions/JsonPathSearchFunction.cs rename to src/Hyperbee.Json/Evaluators/Parser/Node/SearchElementFunction.cs index 8778f380..304a13e8 100644 --- a/src/Hyperbee.Json/Evaluators/Parser/Functions/JsonPathSearchFunction.cs +++ b/src/Hyperbee.Json/Evaluators/Parser/Node/SearchElementFunction.cs @@ -1,24 +1,22 @@ using System.Linq.Expressions; using System.Reflection; -using System.Text.Json; using System.Text.Json.Nodes; using System.Text.RegularExpressions; -namespace Hyperbee.Json.Evaluators.Parser.Functions; +namespace Hyperbee.Json.Evaluators.Parser.Node; -public class JsonPathSearchFunction( string methodName, IList arguments, ParseExpressionContext context ) : ParserExpressionFunction( methodName, arguments, context ) +public class SearchNodeFunction( string methodName, IList arguments, ParseExpressionContext context ) : FilterExpressionFunction( methodName, arguments, context ) { public const string Name = "search"; - // ReSharper disable once StaticMemberInGenericType private static readonly MethodInfo SearchMethod; - static JsonPathSearchFunction() + static SearchNodeFunction() { - SearchMethod = typeof( JsonPathSearchFunction ).GetMethod( nameof( Search ), [typeof( TType ), typeof( string )] ); + SearchMethod = typeof( SearchNodeFunction ).GetMethod( nameof( Search ), [typeof( JsonNode ), typeof( string )] ); } - public override Expression GetExpression( string methodName, IList arguments, ParseExpressionContext context ) + public override Expression GetExpression( string methodName, IList arguments, ParseExpressionContext context ) { if ( arguments.Count != 2 ) { @@ -30,29 +28,18 @@ public override Expression GetExpression( string methodName, IList argum return Expression.Call( SearchMethod, - Expression.Call( JsonPathHelper.GetFirstElementMethod, + Expression.Call( FilterNodeHelper.SelectFirstMethod, context.Current, context.Root, queryExp ) , regex ); } - - public static bool Search( JsonElement element, string regex ) - { - var regexPattern = new Regex( regex.Trim( '\"', '\'' ) ); - var value = element.GetString(); - - // TODO: Talk to BF about how search is different from match - return value != null && regexPattern.IsMatch( value ); - } - public static bool Search( JsonNode node, string regex ) { var regexPattern = new Regex( regex.Trim( '\"', '\'' ) ); var value = node.GetValue(); - // TODO: Talk to BF about how search is different from match return value != null && regexPattern.IsMatch( value ); } diff --git a/src/Hyperbee.Json/Evaluators/Parser/Node/ValueElementFunction.cs b/src/Hyperbee.Json/Evaluators/Parser/Node/ValueElementFunction.cs new file mode 100644 index 00000000..aa30a88e --- /dev/null +++ b/src/Hyperbee.Json/Evaluators/Parser/Node/ValueElementFunction.cs @@ -0,0 +1,24 @@ +using System.Linq.Expressions; + +namespace Hyperbee.Json.Evaluators.Parser.Node; + +public class ValueNodeFunction( string methodName, IList arguments, ParseExpressionContext context ) : FilterExpressionFunction( methodName, arguments, context ) +{ + public const string Name = "value"; + + public override Expression GetExpression( string methodName, IList arguments, ParseExpressionContext context ) + { + if ( arguments.Count != 1 ) + { + return Expression.Throw( Expression.Constant( new ArgumentException( $"{Name} function has invalid parameter count." ) ) ); + } + + var queryExp = Expression.Constant( arguments[0] ); + + return Expression.Call( + FilterNodeHelper.SelectFirstElementValueMethod, + context.Current, + context.Root, + queryExp ); + } +} diff --git a/src/Hyperbee.Json/Evaluators/Parser/Functions/ParenFunction.cs b/src/Hyperbee.Json/Evaluators/Parser/ParenFunction.cs similarity index 64% rename from src/Hyperbee.Json/Evaluators/Parser/Functions/ParenFunction.cs rename to src/Hyperbee.Json/Evaluators/Parser/ParenFunction.cs index e1bcbe0d..a12f6974 100644 --- a/src/Hyperbee.Json/Evaluators/Parser/Functions/ParenFunction.cs +++ b/src/Hyperbee.Json/Evaluators/Parser/ParenFunction.cs @@ -1,8 +1,8 @@ using System.Linq.Expressions; -namespace Hyperbee.Json.Evaluators.Parser.Functions; +namespace Hyperbee.Json.Evaluators.Parser; -public class ParenFunction( ParseExpressionContext context ) : ParserFunction +internal class ParenFunction( ParseExpressionContext context ) : FilterFunction { protected override Expression Evaluate( ReadOnlySpan data, ReadOnlySpan item, ref int start, ref int from ) { diff --git a/src/Hyperbee.Json/Evaluators/Parser/ParseExpressionContext.cs b/src/Hyperbee.Json/Evaluators/Parser/ParseExpressionContext.cs index 82e8db4e..c262978d 100644 --- a/src/Hyperbee.Json/Evaluators/Parser/ParseExpressionContext.cs +++ b/src/Hyperbee.Json/Evaluators/Parser/ParseExpressionContext.cs @@ -2,4 +2,8 @@ namespace Hyperbee.Json.Evaluators.Parser; -public record ParseExpressionContext( Expression Current, Expression Root ); +public record ParseExpressionContext( + Expression Current, + Expression Root, + IJsonTypeDescriptor Descriptor +); diff --git a/src/Hyperbee.Json/Extensions/JsonPathSelectExtensions.cs b/src/Hyperbee.Json/Extensions/JsonPathSelectExtensions.cs index ce07a2f4..e01f6abf 100644 --- a/src/Hyperbee.Json/Extensions/JsonPathSelectExtensions.cs +++ b/src/Hyperbee.Json/Extensions/JsonPathSelectExtensions.cs @@ -7,17 +7,16 @@ public static class JsonPathSelectExtensions { public static IEnumerable Select( this JsonElement element, string query ) { - return new JsonPath().Select( element, query ); + return new JsonPath().Select( element, query ); } public static IEnumerable Select( this JsonDocument document, string query ) { - return new JsonPath().Select( document.RootElement, query ); + return new JsonPath().Select( document.RootElement, query ); } public static IEnumerable Select( this JsonNode node, string query ) { - return new Nodes.JsonPathNode().Select( node, query ); + return new JsonPath().Select( node, query ); } } - diff --git a/src/Hyperbee.Json/JsonPath.cs b/src/Hyperbee.Json/JsonPath.cs index 5fce4439..6d228082 100644 --- a/src/Hyperbee.Json/JsonPath.cs +++ b/src/Hyperbee.Json/JsonPath.cs @@ -1,21 +1,278 @@ -using System.Text.Json; +#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; +using Hyperbee.Json.Tokenizer; namespace Hyperbee.Json; -public sealed class JsonPath +// 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 FilterEvaluator { get; set; } = new JsonPathExpressionEvaluator(); + private static readonly IJsonTypeDescriptor Descriptor = JsonTypeRegistry.GetDescriptor(); + + private static readonly IJsonValueAccessor Accessor = Descriptor.GetAccessor(); - private readonly JsonPathVisitorBase _visitor = new JsonPathElementVisitor(); + private static readonly IJsonPathFilterEvaluator FilterEvaluator = Descriptor.GetFilterEvaluator(); - public IEnumerable Select( in JsonElement value, string query ) + public IEnumerable Select( in TElement value, string query ) { - return _visitor.ExpressionVisitor( value, value, query, FilterEvaluator ); + return EnumerateMatches( value, value, query ); + } + + internal static IEnumerable Select( in TElement value, TElement root, string query ) + { + return EnumerateMatches( value, root, query ); + } + + private static IEnumerable EnumerateMatches( in TElement value, in TElement root, string query ) + { + ArgumentException.ThrowIfNullOrWhiteSpace( query ); + + // quick out + + if ( query == "$" ) + return [value]; + + // tokenize + + var tokens = JsonPathQueryTokenizer.Tokenize( query ); + + if ( !tokens.IsEmpty ) + { + var selector = tokens.Peek().FirstSelector; + + if ( selector == "$" || selector == "@" ) + tokens = tokens.Pop(); + } + + return EnumerateMatches( root, new ElementArgs( value, tokens ) ); } - internal IEnumerable Select( in JsonElement value, JsonElement root, string query ) + private static IEnumerable EnumerateMatches( TElement root, ElementArgs args ) { - return _visitor.ExpressionVisitor( value, root, query, FilterEvaluator ); + var stack = new Stack( 4 ); + + do + { + // deconstruct the next args node + + var (current, tokens) = args; + + if ( tokens.IsEmpty ) + { + yield return current; + continue; + } + + // pop the next token from the stack + + tokens = tokens.Pop( out var token ); + var selector = token.FirstSelector; + + // make sure we have a complex value + + if ( !Accessor.IsObjectOrArray( current ) ) + throw new InvalidOperationException( "Object or Array expected." ); + + // try to access object or array using KEY value + + if ( token.Singular ) + { + if ( Accessor.TryGetChildValue( current, selector, out var childValue ) ) + Push( stack, childValue, tokens ); + + continue; + } + + // wildcard + + if ( selector == "*" ) + { + foreach ( var (_, childKey) in Accessor.EnumerateChildValues( current ) ) + { + Push( stack, current, tokens.Push( new( childKey, SelectorKind.UnspecifiedSingular ) ) ); // (Dot | Index) + } + + continue; + } + + // descendant + + if ( selector == ".." ) + { + foreach ( var (childValue, _) in Accessor.EnumerateChildValues( current ) ) + { + if ( Accessor.IsObjectOrArray( childValue ) ) + Push( stack, childValue, tokens.Push( new( "..", SelectorKind.UnspecifiedGroup ) ) ); // Descendant + } + + Push( stack, current, tokens ); + continue; + } + + // union + + 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 ( 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; + + Push( stack, current, tokens.Push( new( evalSelector, selectorKind ) ) ); + continue; + } + + // [?(exp)] + + if ( childSelector.Length > 3 && childSelector[0] == '?' && childSelector[1] == '(' && childSelector[^1] == ')' ) + { + foreach ( var (childValue, childKey) in Accessor.EnumerateChildValues( current ) ) + { + 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 ) ) + Push( stack, current, tokens.Push( new( childKey, SelectorKind.UnspecifiedSingular ) ) ); // (Name | Index) + } + + continue; + } + + // [name1,name2,...] or [#,#,...] or [start:end:step] + + if ( Accessor.IsArray( current, out var length ) ) + { + if ( JsonPathRegex.RegexNumber().IsMatch( childSelector ) ) + { + // [#,#,...] + Push( stack, Accessor.GetElementAt( current, int.Parse( childSelector ) ), tokens ); + continue; + } + + // [start:end:step] Python slice syntax + if ( JsonPathRegex.RegexSlice().IsMatch( childSelector ) ) + { + foreach ( var index in EnumerateSlice( current, childSelector ) ) + Push( stack, Accessor.GetElementAt( current, index ), tokens ); + continue; + } + + // [name1,name2,...] + foreach ( var index in EnumerateArrayIndices( length ) ) + Push( stack, Accessor.GetElementAt( current, index ), tokens.Push( new( childSelector, SelectorKind.UnspecifiedSingular ) ) ); // Name + + continue; + } + + // [name1,name2,...] + + if ( Accessor.IsObject( current ) ) + { + if ( JsonPathRegex.RegexSlice().IsMatch( childSelector ) || JsonPathRegex.RegexNumber().IsMatch( childSelector ) ) + continue; + + // [name1,name2,...] + if ( Accessor.TryGetChildValue( current, childSelector, out var childValue ) ) + Push( stack, childValue, tokens ); + } + } + + } while ( stack.TryPop( out args ) ); + + yield break; + + static void Push( Stack s, in TElement v, in IImmutableStack t ) => s.Push( new ElementArgs( v, t ) ); + } + + private static IEnumerable EnumerateArrayIndices( int length ) + { + for ( var index = length - 1; index >= 0; index-- ) + yield return index; + } + + private static IEnumerable EnumerateSlice( TElement value, string sliceExpr ) + { + if ( !Accessor.IsArray( value, out var length ) ) + yield break; + + var (lower, upper, step) = SliceSyntaxHelper.ParseExpression( sliceExpr, length, reverse: true ); + + switch ( step ) + { + case 0: + { + yield break; + } + case > 0: + { + for ( var index = lower; index < upper; index += step ) + yield return index; + break; + } + case < 0: + { + for ( var index = upper; index > lower; index += step ) + yield return index; + break; + } + } + } + + private sealed class ElementArgs( in TElement value, in IImmutableStack tokens ) + { + public readonly TElement Value = value; + public readonly IImmutableStack Tokens = tokens; + + public void Deconstruct( out TElement value, out IImmutableStack tokens ) + { + value = Value; + tokens = Tokens; + } } } diff --git a/src/Hyperbee.Json/JsonPathVisitorBase.cs b/src/Hyperbee.Json/JsonPathVisitorBase.cs deleted file mode 100644 index 01f3db4a..00000000 --- a/src/Hyperbee.Json/JsonPathVisitorBase.cs +++ /dev/null @@ -1,277 +0,0 @@ -#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; -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 abstract class JsonPathVisitorBase -{ - internal IEnumerable ExpressionVisitor( in TElement value, in TElement root, string query, IJsonPathFilterEvaluator filterEvaluator ) - { - if ( string.IsNullOrWhiteSpace( query ) ) - throw new ArgumentNullException( nameof( query ) ); - - if ( filterEvaluator == null ) - throw new ArgumentNullException( nameof( filterEvaluator ) ); - - // quick out - - if ( query == "$" ) - return [value]; - - // 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 - { - // deconstruct the next args node - - var (current, tokens) = args; - - if ( tokens.IsEmpty ) - { - yield return current; - continue; - } - - // pop the next token from the stack - - tokens = tokens.Pop( out var token ); - var selector = token.FirstSelector; - - // make sure we have a complex value - - if ( !IsObjectOrArray( current ) ) - throw new InvalidOperationException( "Object or Array expected." ); - - // try to access object or array using KEY value - - if ( token.Singular ) - { - if ( TryGetChildValue( current, selector, out var childValue ) ) - Push( stack, childValue, tokens ); - - continue; - } - - // wildcard - - if ( selector == "*" ) - { - foreach ( var (_, childKey) in EnumerateChildValues( current ) ) - { - Push( stack, current, tokens.Push( new( childKey, SelectorKind.UnspecifiedSingular ) ) ); // (Dot | Index) - } - - continue; - } - - // descendant - - if ( selector == ".." ) - { - foreach ( var (childValue, _) in EnumerateChildValues( current ) ) - { - if ( IsObjectOrArray( childValue ) ) - Push( stack, childValue, tokens.Push( new( "..", SelectorKind.UnspecifiedGroup ) ) ); // Descendant - } - - Push( stack, current, tokens ); - continue; - } - - // union - - 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 ( 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; - - Push( stack, current, tokens.Push( new( evalSelector, selectorKind ) ) ); - continue; - } - - // [?(exp)] - - if ( childSelector.Length > 3 && childSelector[0] == '?' && childSelector[1] == '(' && childSelector[^1] == ')' ) - { - foreach ( var (childValue, childKey) in EnumerateChildValues( current ) ) - { - 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 ) ) - Push( stack, current, tokens.Push( new( childKey, SelectorKind.UnspecifiedSingular ) ) ); // (Name | Index) - } - - continue; - } - - // [name1,name2,...] or [#,#,...] or [start:end:step] - - if ( IsArray( current, out var length ) ) - { - if ( JsonPathRegex.RegexNumber().IsMatch( childSelector ) ) - { - // [#,#,...] - Push( stack, GetElementAt( current, int.Parse( childSelector ) ), tokens ); - continue; - } - - // [start:end:step] Python slice syntax - if ( JsonPathRegex.RegexSlice().IsMatch( childSelector ) ) - { - foreach ( var index in EnumerateSlice( current, childSelector ) ) - Push( stack, GetElementAt( current, index ), tokens ); - continue; - } - - // [name1,name2,...] - foreach ( var index in EnumerateArrayIndices( length ) ) - Push( stack, GetElementAt( current, index ), tokens.Push( new( childSelector, SelectorKind.UnspecifiedSingular ) ) ); // Name - - continue; - } - - // [name1,name2,...] - - if ( IsObject( current ) ) - { - if ( JsonPathRegex.RegexSlice().IsMatch( childSelector ) || JsonPathRegex.RegexNumber().IsMatch( childSelector ) ) - continue; - - // [name1,name2,...] - if ( TryGetChildValue( current, childSelector, out var childValue ) ) - Push( stack, childValue, tokens ); - } - } - - } 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 ) - { - for ( var index = length - 1; index >= 0; index-- ) - yield return index; - } - - private IEnumerable EnumerateSlice( TElement value, string sliceExpr ) - { - if ( !IsArray( value, out var length ) ) - yield break; - - var (lower, upper, step) = SliceSyntaxHelper.ParseExpression( sliceExpr, length, reverse: true ); - - switch ( step ) - { - case 0: - { - yield break; - } - case > 0: - { - for ( var index = lower; index < upper; index += step ) - yield return index; - break; - } - case < 0: - { - for ( var index = upper; index > lower; index += step ) - yield return index; - break; - } - } - } - - // 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 - - private sealed class VisitorArgs( in TElement value, in IImmutableStack tokens ) - { - public readonly TElement Value = value; - public readonly IImmutableStack Tokens = tokens; - - public void Deconstruct( out TElement value, out IImmutableStack tokens ) - { - value = Value; - tokens = Tokens; - } - } -} diff --git a/src/Hyperbee.Json/JsonTypeRegistry.cs b/src/Hyperbee.Json/JsonTypeRegistry.cs new file mode 100644 index 00000000..e0ec2c18 --- /dev/null +++ b/src/Hyperbee.Json/JsonTypeRegistry.cs @@ -0,0 +1,33 @@ +using System.Text.Json; +using System.Text.Json.Nodes; +using Hyperbee.Json.Evaluators; +using Hyperbee.Json.Evaluators.Parser.Element; +using Hyperbee.Json.Evaluators.Parser.Node; + +namespace Hyperbee.Json; + +public class JsonTypeRegistry +{ + private static readonly Dictionary Descriptors = []; + + static JsonTypeRegistry() + { + Register( new JsonElementTypeDescriptor() ); + Register( new JsonNodeTypeDescriptor() ); + } + + public static void Register( IJsonTypeDescriptor descriptor ) + { + Descriptors[typeof( TElement )] = descriptor; + } + + public static IJsonTypeDescriptor GetDescriptor() + { + if ( Descriptors.TryGetValue( typeof( TElement ), out var descriptor ) ) + { + return descriptor; + } + + throw new InvalidOperationException( $"No JSON descriptors registered for type {typeof( TElement )}." ); + } +} diff --git a/src/Hyperbee.Json/Nodes/JsonPath.cs b/src/Hyperbee.Json/Nodes/JsonPath.cs deleted file mode 100644 index 1e0852cb..00000000 --- a/src/Hyperbee.Json/Nodes/JsonPath.cs +++ /dev/null @@ -1,21 +0,0 @@ -using System.Text.Json.Nodes; -using Hyperbee.Json.Evaluators; - -namespace Hyperbee.Json.Nodes; - -public sealed class JsonPathNode -{ - public static IJsonPathFilterEvaluator FilterEvaluator { get; set; } = new JsonPathExpressionEvaluator(); - - private readonly JsonPathVisitorBase _visitor = new JsonPathNodeVisitor(); - - public IEnumerable Select( in JsonNode value, string query ) - { - return _visitor.ExpressionVisitor( value, value, query, FilterEvaluator ); - } - - internal IEnumerable Select( in JsonNode value, JsonNode root, string query ) - { - return _visitor.ExpressionVisitor( value, root, query, FilterEvaluator ); - } -} diff --git a/test/Hyperbee.Json.Benchmark/Hyperbee.Json.Benchmark.csproj b/test/Hyperbee.Json.Benchmark/Hyperbee.Json.Benchmark.csproj index fd760c69..9c8b65e1 100644 --- a/test/Hyperbee.Json.Benchmark/Hyperbee.Json.Benchmark.csproj +++ b/test/Hyperbee.Json.Benchmark/Hyperbee.Json.Benchmark.csproj @@ -15,6 +15,7 @@ + diff --git a/test/Hyperbee.Json.Benchmark/JsonPathExpressionParser.cs b/test/Hyperbee.Json.Benchmark/JsonPathExpressionParser.cs index 475b7dca..30b4e103 100644 --- a/test/Hyperbee.Json.Benchmark/JsonPathExpressionParser.cs +++ b/test/Hyperbee.Json.Benchmark/JsonPathExpressionParser.cs @@ -3,13 +3,15 @@ using System.Text.Json.Nodes; using BenchmarkDotNet.Attributes; using Hyperbee.Json.Evaluators.Parser; +using Hyperbee.Json.Evaluators.Parser.Element; +using Hyperbee.Json.Evaluators.Parser.Node; namespace Hyperbee.Json.Benchmark; public class JsonPathExpressionParser { - private ParseExpressionContext _nodeExpressionContext; - private ParseExpressionContext _elementExpressionContext; + private ParseExpressionContext _nodeExpressionContext; + private ParseExpressionContext _elementExpressionContext; [Params( "(\"world\" == 'world') && (true || false)" )] public string Filter; @@ -18,13 +20,15 @@ public class JsonPathExpressionParser [GlobalSetup] public void Setup() { - _nodeExpressionContext = new ParseExpressionContext( + _nodeExpressionContext = new ParseExpressionContext( Expression.Parameter( typeof( JsonNode ) ), - Expression.Parameter( typeof( JsonNode ) ) ); + Expression.Parameter( typeof( JsonNode ) ), + new JsonNodeTypeDescriptor() ); - _elementExpressionContext = new ParseExpressionContext( + _elementExpressionContext = new ParseExpressionContext( + Expression.Parameter( typeof( JsonElement ) ), Expression.Parameter( typeof( JsonElement ) ), - Expression.Parameter( typeof( JsonElement ) ) ); + new JsonElementTypeDescriptor() ); } [Benchmark] diff --git a/test/Hyperbee.Json.Benchmark/JsonPathParseAndSelect.cs b/test/Hyperbee.Json.Benchmark/JsonPathParseAndSelect.cs index 9edd22ba..8934095f 100644 --- a/test/Hyperbee.Json.Benchmark/JsonPathParseAndSelect.cs +++ b/test/Hyperbee.Json.Benchmark/JsonPathParseAndSelect.cs @@ -1,9 +1,9 @@ using System.Text.Json; using System.Text.Json.Nodes; using BenchmarkDotNet.Attributes; -using Hyperbee.Json.Evaluators; using Hyperbee.Json.Extensions; using Newtonsoft.Json.Linq; +using JsonEverything = Json.Path; namespace Hyperbee.Json.Benchmark; @@ -59,14 +59,14 @@ public class JsonPathParseAndSelect public string Document; [Benchmark] - public void JsonPath_ExpressionEvaluator_JsonElement() + public void JsonPath_Hyperbee_JsonElement() { var element = JsonDocument.Parse( Document ).RootElement; var _ = element.Select( Filter ).ToArray(); } [Benchmark] - public void JsonPath_ExpressionEvaluator_JsonNode() + public void JsonPath_Hyperbee_JsonNode() { var node = JsonNode.Parse( Document )!; var _ = node.Select( Filter ).ToArray(); @@ -78,4 +78,14 @@ public void JsonPath_Newtonsoft_JObject() var jObject = JObject.Parse( Document ); var _ = jObject.SelectTokens( Filter ).ToArray(); } + + [Benchmark] + public void JsonPath_JsonEverything_JsonNode() + { + + var path = JsonEverything.JsonPath.Parse( Filter ); + var node = JsonNode.Parse( Document )!; + var _ = path.Evaluate( node ).Matches!.ToArray(); + } + } diff --git a/test/Hyperbee.Json.Benchmark/JsonPathSelectEvaluator.cs b/test/Hyperbee.Json.Benchmark/JsonPathSelectEvaluator.cs index 07545273..6be899f0 100644 --- a/test/Hyperbee.Json.Benchmark/JsonPathSelectEvaluator.cs +++ b/test/Hyperbee.Json.Benchmark/JsonPathSelectEvaluator.cs @@ -80,13 +80,13 @@ public void Setup() } [Benchmark] - public void JsonPath_ExpressionEvaluator_JsonElement() + public void JsonPath_Hyperbee_JsonElement() { var _ = _element.Select( Filter ).ToArray(); } [Benchmark] - public void JsonPath_ExpressionEvaluator_JsonNode() + public void JsonPath_Hyperbee_JsonNode() { var _ = _node.Select( Filter ).ToArray(); } diff --git a/test/Hyperbee.Json.Tests/Evaluators/JsonPathExpressionTests.cs b/test/Hyperbee.Json.Tests/Evaluators/JsonPathExpressionTests.cs index e6df4bf7..f48a4b35 100644 --- a/test/Hyperbee.Json.Tests/Evaluators/JsonPathExpressionTests.cs +++ b/test/Hyperbee.Json.Tests/Evaluators/JsonPathExpressionTests.cs @@ -4,6 +4,8 @@ using System.Text.Json; using System.Text.Json.Nodes; using Hyperbee.Json.Evaluators.Parser; +using Hyperbee.Json.Evaluators.Parser.Element; +using Hyperbee.Json.Evaluators.Parser.Node; using Hyperbee.Json.Extensions; using Hyperbee.Json.Tests.TestSupport; using Microsoft.VisualStudio.TestTools.UnitTesting; @@ -151,12 +153,14 @@ private static (Expression, ParameterExpression) GetExpression( string filter, T { var param = Expression.Parameter( sourceType ); var expression = sourceType == typeof( JsonElement ) - ? JsonPathExpression.Parse( filter, new ParseExpressionContext( + ? JsonPathExpression.Parse( filter, new ParseExpressionContext( param, - param ) ) - : JsonPathExpression.Parse( filter, new ParseExpressionContext( param, - param ) ); + new JsonElementTypeDescriptor() ) ) + : JsonPathExpression.Parse( filter, new ParseExpressionContext( + param, + param, + new JsonNodeTypeDescriptor() ) ); return (expression, param); } @@ -190,7 +194,7 @@ private static bool CompileAndExecute( string filter, Type sourceType ) if ( sourceType == typeof( JsonElement ) ) { var source = GetDocument(); - var func = JsonPathExpression.Compile( filter ); + var func = JsonPathExpression.Compile( filter, new JsonElementTypeDescriptor() ); return func( source.RootElement, source.RootElement ); } @@ -198,7 +202,7 @@ private static bool CompileAndExecute( string filter, Type sourceType ) { // arrange var source = GetDocument(); - var func = JsonPathExpression.Compile( filter ); + var func = JsonPathExpression.Compile( filter, new JsonNodeTypeDescriptor() ); // act return func( source, source );