From a25ca6446367b1e601cd4d3d35ae1cd794ea1192 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 25 Jun 2024 17:23:53 -0400 Subject: [PATCH] [FEATURE]: Function Registration (#20) * Add custom function example and test * Made FilterParser generic with TNode * Context maybe everything, but it need not be everywhere * Separated parser expressions from extension functions * Improved whitespace handling * Switching to ParserState * Simplified Function Signatures * Removed base FilterFunction and changed GetSelectFunction so it has a default implementation * Simplified TypeDescriptor and simplified Filter compare and converting. --------- Co-authored-by: Brenton Farmer Co-authored-by: Matt Edwards --- README.md | 150 +++--- docs/ADDITIONAL-CLASSES.md | 10 +- .../Element/ElementTypeDescriptor.cs | 44 +- .../Element/ElementValueAccessor.cs | 6 + .../Element/Functions/CountElementFunction.cs | 4 +- .../Functions/LengthElementFunction.cs | 4 +- .../Element/Functions/MatchElementFunction.cs | 13 +- .../Functions/SearchElementFunction.cs | 11 +- .../Functions/SelectElementFunction.cs | 22 - .../Element/Functions/ValueElementFunction.cs | 21 +- .../Descriptors/FunctionRegistry.cs | 19 + .../Descriptors/ITypeDescriptor.cs | 15 +- .../Descriptors/IValueAccessor.cs | 1 + .../Node/Functions/CountNodeFunction.cs | 4 +- .../Node/Functions/LengthNodeFunction.cs | 4 +- .../Node/Functions/MatchNodeFunction.cs | 13 +- .../Node/Functions/SearchNodeFunction.cs | 11 +- .../Node/Functions/SelectNodeFunction.cs | 22 - .../Node/Functions/ValueNodeFunction.cs | 22 +- .../Descriptors/Node/NodeTypeDescriptor.cs | 42 +- .../Descriptors/Node/NodeValueAccessor.cs | 8 + .../JsonPropertyFromPathExtensions.cs | 4 +- src/Hyperbee.Json/Filters/FilterEvaluator.cs | 7 +- .../Expressions/FunctionExpressionFactory.cs | 20 + .../Parser/Expressions/IExpressionFactory.cs | 8 + .../Expressions/LiteralExpressionFactory.cs | 39 ++ .../Expressions/NotExpressionFactory.cs | 12 + .../Expressions/ParenExpressionFactory.cs | 19 + .../Expressions/SelectExpressionFactory.cs | 35 ++ .../Filters/Parser/FilterContext.cs | 16 + .../Filters/Parser/FilterExpressionParser.cs | 363 --------------- .../Filters/Parser/FilterExtensionFunction.cs | 29 +- .../Filters/Parser/FilterFunction.cs | 83 ---- .../Filters/Parser/FilterParser.cs | 438 ++++++++++++++++++ .../Filters/Parser/FilterTruthyExpression.cs | 45 +- .../Filters/Parser/LiteralFunction.cs | 48 -- src/Hyperbee.Json/Filters/Parser/Operator.cs | 17 + .../Filters/Parser/ParenFunction.cs | 11 - .../Filters/Parser/ParseExpressionContext.cs | 10 - .../Filters/Parser/ParserState.cs | 30 ++ src/Hyperbee.Json/Hyperbee.Json.csproj | 3 + src/Hyperbee.Json/JsonPath.cs | 5 +- src/Hyperbee.Json/JsonPathQueryParser.cs | 2 - ...JsonPathBuilder.cs => JsonPathResolver.cs} | 6 +- src/Hyperbee.Json/JsonPathSegment.cs | 12 +- .../JsonTypeDescriptorRegistry.cs | 2 +- .../FilterExpressionParserEvaluator.cs | 19 +- .../Evaluators/FilterExpressionParserTests.cs | 44 +- .../FilterExtenstionFunctionTests.cs | 53 +++ .../JsonPathBuilderTests.cs | 4 +- 50 files changed, 994 insertions(+), 836 deletions(-) delete mode 100644 src/Hyperbee.Json/Descriptors/Element/Functions/SelectElementFunction.cs create mode 100644 src/Hyperbee.Json/Descriptors/FunctionRegistry.cs delete mode 100644 src/Hyperbee.Json/Descriptors/Node/Functions/SelectNodeFunction.cs create mode 100644 src/Hyperbee.Json/Filters/Parser/Expressions/FunctionExpressionFactory.cs create mode 100644 src/Hyperbee.Json/Filters/Parser/Expressions/IExpressionFactory.cs create mode 100644 src/Hyperbee.Json/Filters/Parser/Expressions/LiteralExpressionFactory.cs create mode 100644 src/Hyperbee.Json/Filters/Parser/Expressions/NotExpressionFactory.cs create mode 100644 src/Hyperbee.Json/Filters/Parser/Expressions/ParenExpressionFactory.cs create mode 100644 src/Hyperbee.Json/Filters/Parser/Expressions/SelectExpressionFactory.cs create mode 100644 src/Hyperbee.Json/Filters/Parser/FilterContext.cs delete mode 100644 src/Hyperbee.Json/Filters/Parser/FilterExpressionParser.cs delete mode 100644 src/Hyperbee.Json/Filters/Parser/FilterFunction.cs create mode 100644 src/Hyperbee.Json/Filters/Parser/FilterParser.cs delete mode 100644 src/Hyperbee.Json/Filters/Parser/LiteralFunction.cs create mode 100644 src/Hyperbee.Json/Filters/Parser/Operator.cs delete mode 100644 src/Hyperbee.Json/Filters/Parser/ParenFunction.cs delete mode 100644 src/Hyperbee.Json/Filters/Parser/ParseExpressionContext.cs create mode 100644 src/Hyperbee.Json/Filters/Parser/ParserState.cs rename src/Hyperbee.Json/{JsonPathBuilder.cs => JsonPathResolver.cs} (97%) create mode 100644 test/Hyperbee.Json.Tests/Evaluators/FilterExtenstionFunctionTests.cs rename test/Hyperbee.Json.Tests/{Builder => Resolver}/JsonPathBuilderTests.cs (90%) diff --git a/README.md b/README.md index e0fbf4dc..613fd1d3 100644 --- a/README.md +++ b/README.md @@ -2,15 +2,15 @@ # Hyperbee.Json `Hyperbee.Json` is a high-performance JSONPath parser for .NET, that supports both `JsonElement` and `JsonNode`. -The library is designed to be quick and extensible, allowing support for other JSON document types. +The library is designed to be quick and extensible, allowing support for other JSON document types and functions. ## Features - **High Performance:** Optimized for performance and efficiency. - **Supports:** `JsonElement` and `JsonNode`. -- **Extensible:** Easily extended to support additional JSON document types. +- **Extensible:** Easily extended to support additional JSON document types and functions. - **`IEnumerable` Results:** Deferred execution queries with `IEnumerable`. -- **Comformant:** Adheres to the JSONPath Specification [RFC 9535](https://www.rfc-editor.org/rfc/rfc9535.html). +- **Conformant:** Adheres to the JSONPath Specification [RFC 9535](https://www.rfc-editor.org/rfc/rfc9535.html). ## JSONPath Consensus @@ -80,8 +80,6 @@ foreach (var item in result) } ``` -### Advanced Examples - #### Filtering ```csharp @@ -139,7 +137,7 @@ Console.WriteLine(result.First()); // Output: "fiction" ## JSONPath Syntax Reference -Here's a quick reference for JSONPath syntax supported by Hyperbee.Json: +Here's a quick reference for JSONPath syntax: | JSONPath | Description |:---------------------------------------------|:----------------------------------------------------------- @@ -179,11 +177,9 @@ the syntax `?()`, as in: $.store.book[?(@.price < 10)].title -### Supported JSONPath Methods - -JsonPath expressions support basic methods calls. By default, Hyperbee supports the methods defined in the RFC. -You can extend the supported funtion set by creating your own functions. +### JSONPath Methods +JsonPath expressions support basic methods calls. | Method | Description | Example |------------|--------------------------------------------------------|------------------------------------------------ @@ -193,11 +189,91 @@ You can extend the supported funtion set by creating your own functions. | `search()` | Searches for a string within another string. | `$.store.book[?(@.title.search('Sword'))]` | `value()` | Accesses the value of a key in the current object. | `$.store.book[?(@.price.value() < 10)]` -## Additional Documentation -Additional documentation can be found in the project's `/docs` folder. +You can extend the supported function set by registering your own functions. + +#### Example: `JsonNode` Path Function + +**Step 1:** Create a custom function that returns the path of a `JsonNode`. + +```csharp +public class PathNodeFunction() : FilterExtensionFunction( argumentCount: 1 ) +{ + public const string Name = "path"; + private static readonly Expression PathExpression = Expression.Constant( (Func, string>) Path ); + + protected override Expression GetExtensionExpression( Expression[] arguments ) + { + return Expression.Invoke( PathExpression, arguments[0] ); + } + + public static string Path( IEnumerable nodes ) + { + var node = nodes.FirstOrDefault(); + return node?.GetPath(); + } +} +``` + +**Step 2:** Register your custom function. + +```csharp +JsonTypeDescriptorRegistry.GetDescriptor().Functions + .Register( PathNodeFunction.Name, () => new PathNodeFunction() ); +``` + +**Step 3:** Use your custom function in a JSONPath query. + +```csharp +var results = source.Select( "$..[?path(@) == '$.store.book[2].title']" ); +``` + +## Comparison with Other Libraries + +There are excellent libraries available for RFC-9535 .NET JsonPath. + +### [JsonPath.Net](https://docs.json-everything.net/path/basics/) Json-Everything + +- **Pros:** + - Extensive JSON ecosystem. + - Comprehensive feature set. + - Deferred execution queries with `IEnumerable`. + - Strong community support. + +- **Cons:** + - No support for `JsonElement`. + - Not quite as fast as other `System.Text.Json` implementations. + +### [JsonCons.NET](https://danielaparker.github.io/JsonCons.Net/articles/JsonPath/JsonConsJsonPath.html) + +- **Pros:** + - High performance. + - Enhanced JsonPath syntax. + +- **Cons:** + - No support for `JsonNode`. + - Does not return an `IEnumerable` result (no defered query execution). + +### [Json.NET](https://www.newtonsoft.com/json) Newtonsoft + +- **Pros:** + - Comprehensive feature set. + - Documentation and examples. + - Strong community support. + - Level 2 .NET Foundation Project. + +- **Cons:** + - No support for `JsonElement`, or `JsonNode`. + +### Why Choose [Hyperbee.Json](https://github.com/Stillpoint-Software/Hyperbee.Json) ? + +- High Performance. +- Supports both `JsonElement`, and `JsonNode`. +- Deferred execution queries with `IEnumerable`. +- Extendable to support additional JSON document types and functions. +- Consensus focused JSONPath implementation. -## Benchmarks +- ## Benchmarks Here is a performance comparison of various queries on the standard book store document. @@ -240,6 +316,7 @@ Here is a performance comparison of various queries on the standard book store d } ``` +``` | Method | Filter | Mean | Error | StdDev | Allocated |:----------------------- |:-------------------------------- |:--------- |:---------- |:--------- |:--------- | Hyperbee_JsonElement | $..* `First()` | 3.042 us | 0.3928 us | 0.0215 us | 3.82 KB @@ -271,52 +348,11 @@ Here is a performance comparison of various queries on the standard book store d | JsonCons_JsonElement | $.store.book[0] | 3.365 us | 10.9259 us | 0.5989 us | 3.21 KB | JsonEverything_JsonNode | $.store.book[0] | 4.670 us | 0.6449 us | 0.0354 us | 5.96 KB | Newtonsoft_JObject | $.store.book[0] | 8.572 us | 1.5455 us | 0.0847 us | 14.56 KB +``` -## Comparison with Other Libraries - -There are excellent options available for RFC-9535 .NET JsonPath. - -### [JsonPath.Net](https://docs.json-everything.net/path/basics/) Json-Everything - -- **Pros:** - - Extensive JSON ecosystem. - - Comprehensive feature set. - - Deferred execution queries with `IEnumerable`. - - Strong community support. - -- **Cons:** - - No support for `JsonElement`. - - Slower performance and higher memory allocation than other `System.Text.Json` implementations. - -### [JsonCons.NET](https://danielaparker.github.io/JsonCons.Net/articles/JsonPath/JsonConsJsonPath.html) - -- **Pros:** - - High performance. - - Enhanced JsonPath syntax. - -- **Cons:** - - No support for `JsonNode`. - - Does not return an `IEnumerable` result (no defered query execution). - making it less efficient, and more memory intensive, for certain operations, - -### [Json.NET](https://www.newtonsoft.com/json) Newtonsoft - -- **Pros:** - - Comprehensive feature set. - - Documentation and examples. - - Level 2 .NET Foundation Project. - -- **Cons:** - - No support for `JsonElement`, or `JsonNode`. - - Slower performance and higher memory allocation than `System.Text.Json`. - -### Why Choose [Hyperbee.Json](https://github.com/Stillpoint-Software/Hyperbee.Json) ? +## Additional Documentation -- High Performance. -- Focus on consensus implementation. -- Supports both `JsonElement`, and `JsonNode`. -- Deferred execution queries with `IEnumerable`. -- Extendable to support additional JSON document types and functions. +Additional documentation can be found in the project's `/docs` folder. ## Credits diff --git a/docs/ADDITIONAL-CLASSES.md b/docs/ADDITIONAL-CLASSES.md index 09fdb48a..6246a912 100644 --- a/docs/ADDITIONAL-CLASSES.md +++ b/docs/ADDITIONAL-CLASSES.md @@ -65,9 +65,9 @@ Examples of valid path syntax: ### JsonElement Path -Unlike `JsonNode`, `JsonElement` does not have a `Path` property. `JsonPathBuilder` will find the path -for a given element. +Unlike `JsonNode`, `JsonElement` does not have a `Path` property. `JsonPathResolver` will find the path +for a given `JsonElement`. -| Method | Description -|:--------------------------|:----------- -| `JsonPathBuilder.GetPath` | Returns the JsonPath location string for a given element +| Method | Description +|:---------------------------|:----------- +| `JsonPathResolver.GetPath` | Returns the JsonPath location string for a given element diff --git a/src/Hyperbee.Json/Descriptors/Element/ElementTypeDescriptor.cs b/src/Hyperbee.Json/Descriptors/Element/ElementTypeDescriptor.cs index 2d1b0388..d58e9bf7 100644 --- a/src/Hyperbee.Json/Descriptors/Element/ElementTypeDescriptor.cs +++ b/src/Hyperbee.Json/Descriptors/Element/ElementTypeDescriptor.cs @@ -1,8 +1,6 @@ -using System.Linq.Expressions; -using System.Text.Json; +using System.Text.Json; using Hyperbee.Json.Descriptors.Element.Functions; using Hyperbee.Json.Filters; -using Hyperbee.Json.Filters.Parser; namespace Hyperbee.Json.Descriptors.Element; @@ -10,41 +8,21 @@ public class ElementTypeDescriptor : ITypeDescriptor { private FilterEvaluator _evaluator; private ElementValueAccessor _accessor; - public Dictionary Functions { get; init; } - public IValueAccessor Accessor - { - get => _accessor ??= new ElementValueAccessor(); - } - - public IFilterEvaluator FilterEvaluator - { - get => _evaluator ??= new FilterEvaluator( this ); - } + public FunctionRegistry Functions { get; } = new(); - public FilterFunction GetSelectFunction( ParseExpressionContext context ) => - new SelectElementFunction( context ); - - public Expression GetValueExpression( Expression expression ) - { - if ( expression is null ) - return null; - - return expression.Type == typeof( IEnumerable ) - ? Expression.Invoke( ValueElementFunction.ValueExpression, expression ) - : expression; - } + public IValueAccessor Accessor => + _accessor ??= new ElementValueAccessor(); + public IFilterEvaluator FilterEvaluator => + _evaluator ??= new FilterEvaluator( this ); public ElementTypeDescriptor() { - Functions = new Dictionary( - [ - new KeyValuePair( CountElementFunction.Name, context => new CountElementFunction( context ) ), - new KeyValuePair( LengthElementFunction.Name, context => new LengthElementFunction( context ) ), - new KeyValuePair( MatchElementFunction.Name, context => new MatchElementFunction( context ) ), - new KeyValuePair( SearchElementFunction.Name, context => new SearchElementFunction( context ) ), - new KeyValuePair( ValueElementFunction.Name, context => new ValueElementFunction( context ) ), - ] ); + Functions.Register( CountElementFunction.Name, () => new CountElementFunction() ); + Functions.Register( LengthElementFunction.Name, () => new LengthElementFunction() ); + Functions.Register( MatchElementFunction.Name, () => new MatchElementFunction() ); + Functions.Register( SearchElementFunction.Name, () => new SearchElementFunction() ); + Functions.Register( ValueElementFunction.Name, () => new ValueElementFunction() ); } } diff --git a/src/Hyperbee.Json/Descriptors/Element/ElementValueAccessor.cs b/src/Hyperbee.Json/Descriptors/Element/ElementValueAccessor.cs index cab956c5..29cf766d 100644 --- a/src/Hyperbee.Json/Descriptors/Element/ElementValueAccessor.cs +++ b/src/Hyperbee.Json/Descriptors/Element/ElementValueAccessor.cs @@ -1,6 +1,7 @@ using System.Globalization; using System.Runtime.CompilerServices; using System.Text.Json; +using Hyperbee.Json.Descriptors.Element.Functions; namespace Hyperbee.Json.Descriptors.Element; @@ -113,4 +114,9 @@ static bool IsPathOperator( ReadOnlySpan x ) }; } } + + public object GetAsValue( IEnumerable elements ) + { + return ValueElementFunction.Value( elements ); + } } diff --git a/src/Hyperbee.Json/Descriptors/Element/Functions/CountElementFunction.cs b/src/Hyperbee.Json/Descriptors/Element/Functions/CountElementFunction.cs index db3a1593..c2073206 100644 --- a/src/Hyperbee.Json/Descriptors/Element/Functions/CountElementFunction.cs +++ b/src/Hyperbee.Json/Descriptors/Element/Functions/CountElementFunction.cs @@ -5,12 +5,12 @@ namespace Hyperbee.Json.Descriptors.Element.Functions; -public class CountElementFunction( ParseExpressionContext context ) : FilterExtensionFunction( argumentCount: 1, context ) +public class CountElementFunction() : FilterExtensionFunction( argumentCount: 1 ) { public const string Name = "count"; private static readonly Expression CountExpression = Expression.Constant( (Func, float>) Count ); - public override Expression GetExtensionExpression( Expression[] arguments, ParseExpressionContext context ) + protected override Expression GetExtensionExpression( Expression[] arguments ) { return Expression.Invoke( CountExpression, arguments[0] ); } diff --git a/src/Hyperbee.Json/Descriptors/Element/Functions/LengthElementFunction.cs b/src/Hyperbee.Json/Descriptors/Element/Functions/LengthElementFunction.cs index d1ae77e4..7a40e282 100644 --- a/src/Hyperbee.Json/Descriptors/Element/Functions/LengthElementFunction.cs +++ b/src/Hyperbee.Json/Descriptors/Element/Functions/LengthElementFunction.cs @@ -4,12 +4,12 @@ namespace Hyperbee.Json.Descriptors.Element.Functions; -public class LengthElementFunction( ParseExpressionContext context ) : FilterExtensionFunction( argumentCount: 1, context ) +public class LengthElementFunction() : FilterExtensionFunction( argumentCount: 1 ) { public const string Name = "length"; private static readonly Expression LengthExpression = Expression.Constant( (Func, float>) Length ); - public override Expression GetExtensionExpression( Expression[] arguments, ParseExpressionContext context ) + protected override Expression GetExtensionExpression( Expression[] arguments ) { return Expression.Invoke( LengthExpression, arguments[0] ); } diff --git a/src/Hyperbee.Json/Descriptors/Element/Functions/MatchElementFunction.cs b/src/Hyperbee.Json/Descriptors/Element/Functions/MatchElementFunction.cs index cab6b2e5..323a6331 100644 --- a/src/Hyperbee.Json/Descriptors/Element/Functions/MatchElementFunction.cs +++ b/src/Hyperbee.Json/Descriptors/Element/Functions/MatchElementFunction.cs @@ -5,27 +5,26 @@ namespace Hyperbee.Json.Descriptors.Element.Functions; -public class MatchElementFunction( ParseExpressionContext context ) : FilterExtensionFunction( argumentCount: 2, context ) +public class MatchElementFunction() : FilterExtensionFunction( argumentCount: 2 ) { public const string Name = "match"; private static readonly Expression MatchExpression = Expression.Constant( (Func, string, bool>) Match ); - public override Expression GetExtensionExpression( Expression[] arguments, ParseExpressionContext context ) + protected override Expression GetExtensionExpression( Expression[] arguments ) { return Expression.Invoke( MatchExpression, arguments[0], arguments[1] ); } public static bool Match( IEnumerable elements, string regex ) { - var elementValue = elements.FirstOrDefault().GetString(); - if ( elementValue == null ) + var value = elements.FirstOrDefault().GetString(); + + if ( value == null ) { return false; } var regexPattern = new Regex( regex.Trim( '\"', '\'' ) ); - var value = $"^{elementValue}$"; - - return regexPattern.IsMatch( value ); + return regexPattern.IsMatch( $"^{value}$" ); } } diff --git a/src/Hyperbee.Json/Descriptors/Element/Functions/SearchElementFunction.cs b/src/Hyperbee.Json/Descriptors/Element/Functions/SearchElementFunction.cs index ed250963..47686c5e 100644 --- a/src/Hyperbee.Json/Descriptors/Element/Functions/SearchElementFunction.cs +++ b/src/Hyperbee.Json/Descriptors/Element/Functions/SearchElementFunction.cs @@ -5,25 +5,26 @@ namespace Hyperbee.Json.Descriptors.Element.Functions; -public class SearchElementFunction( ParseExpressionContext context ) : FilterExtensionFunction( argumentCount: 2, context ) +public class SearchElementFunction() : FilterExtensionFunction( argumentCount: 2 ) { public const string Name = "search"; private static readonly Expression SearchExpression = Expression.Constant( (Func, string, bool>) Search ); - public override Expression GetExtensionExpression( Expression[] arguments, ParseExpressionContext context ) + protected override Expression GetExtensionExpression( Expression[] arguments ) { return Expression.Invoke( SearchExpression, arguments[0], arguments[1] ); } public static bool Search( IEnumerable elements, string regex ) { - var elementValue = elements.FirstOrDefault().GetString(); - if ( elementValue == null ) + var value = elements.FirstOrDefault().GetString(); + + if ( value == null ) { return false; } var regexPattern = new Regex( regex.Trim( '\"', '\'' ) ); - return regexPattern.IsMatch( elementValue ); + return regexPattern.IsMatch( value ); } } diff --git a/src/Hyperbee.Json/Descriptors/Element/Functions/SelectElementFunction.cs b/src/Hyperbee.Json/Descriptors/Element/Functions/SelectElementFunction.cs deleted file mode 100644 index 9c059f96..00000000 --- a/src/Hyperbee.Json/Descriptors/Element/Functions/SelectElementFunction.cs +++ /dev/null @@ -1,22 +0,0 @@ -using System.Linq.Expressions; -using System.Text.Json; -using Hyperbee.Json.Filters.Parser; - -namespace Hyperbee.Json.Descriptors.Element.Functions; - -public class SelectElementFunction( ParseExpressionContext context ) : FilterFunction -{ - private static readonly Expression SelectExpression = Expression.Constant( (Func>) Select ); - - protected override Expression GetExpressionImpl( ReadOnlySpan data, ReadOnlySpan item, ref int start, ref int from ) - { - var queryExp = Expression.Constant( item.ToString() ); - - return Expression.Invoke( SelectExpression, context.Current, context.Root, queryExp ); - } - - public static IEnumerable Select( JsonElement current, JsonElement root, string query ) - { - return JsonPath.Select( current, root, query ); - } -} diff --git a/src/Hyperbee.Json/Descriptors/Element/Functions/ValueElementFunction.cs b/src/Hyperbee.Json/Descriptors/Element/Functions/ValueElementFunction.cs index cf48e503..8e3ce63b 100644 --- a/src/Hyperbee.Json/Descriptors/Element/Functions/ValueElementFunction.cs +++ b/src/Hyperbee.Json/Descriptors/Element/Functions/ValueElementFunction.cs @@ -1,16 +1,15 @@ using System.Linq.Expressions; using System.Text.Json; - using Hyperbee.Json.Filters.Parser; namespace Hyperbee.Json.Descriptors.Element.Functions; -public class ValueElementFunction( ParseExpressionContext context ) : FilterExtensionFunction( argumentCount: 1, context ) +public class ValueElementFunction() : FilterExtensionFunction( argumentCount: 1 ) { public const string Name = "value"; public static readonly Expression ValueExpression = Expression.Constant( (Func, object>) Value ); - public override Expression GetExtensionExpression( Expression[] arguments, ParseExpressionContext context ) + protected override Expression GetExtensionExpression( Expression[] arguments ) { return Expression.Invoke( ValueExpression, arguments[0] ); } @@ -31,15 +30,15 @@ public static object Value( IEnumerable elements ) JsonValueKind.Undefined => false, _ => false }; - } - private static bool IsNotEmpty( JsonElement element ) - { - return element.ValueKind switch + static bool IsNotEmpty( JsonElement element ) { - JsonValueKind.Array => element.EnumerateArray().Any(), - JsonValueKind.Object => element.EnumerateObject().Any(), - _ => false - }; + return element.ValueKind switch + { + JsonValueKind.Array => element.EnumerateArray().Any(), + JsonValueKind.Object => element.EnumerateObject().Any(), + _ => false + }; + } } } diff --git a/src/Hyperbee.Json/Descriptors/FunctionRegistry.cs b/src/Hyperbee.Json/Descriptors/FunctionRegistry.cs new file mode 100644 index 00000000..2d342441 --- /dev/null +++ b/src/Hyperbee.Json/Descriptors/FunctionRegistry.cs @@ -0,0 +1,19 @@ +using Hyperbee.Json.Filters.Parser; + +namespace Hyperbee.Json.Descriptors; + +public sealed class FunctionRegistry +{ + private Dictionary Functions { get; } = []; + + public void Register( string name, Func factory ) + where TFunction : FilterExtensionFunction + { + Functions[name] = () => factory(); + } + + internal bool TryGetCreator( string name, out FunctionCreator functionCreator ) + { + return Functions.TryGetValue( name, out functionCreator ); + } +} diff --git a/src/Hyperbee.Json/Descriptors/ITypeDescriptor.cs b/src/Hyperbee.Json/Descriptors/ITypeDescriptor.cs index 93190191..dcffa35a 100644 --- a/src/Hyperbee.Json/Descriptors/ITypeDescriptor.cs +++ b/src/Hyperbee.Json/Descriptors/ITypeDescriptor.cs @@ -1,21 +1,16 @@ -using System.Linq.Expressions; -using Hyperbee.Json.Filters; +using Hyperbee.Json.Filters; using Hyperbee.Json.Filters.Parser; namespace Hyperbee.Json.Descriptors; -public delegate FilterExtensionFunction FunctionCreator( ParseExpressionContext context ); +public delegate FilterExtensionFunction FunctionCreator(); -public interface IJsonTypeDescriptor +public interface ITypeDescriptor { - public Dictionary Functions { get; } - - public FilterFunction GetSelectFunction( ParseExpressionContext context ); - - public Expression GetValueExpression( Expression context ); + public FunctionRegistry Functions { get; } } -public interface ITypeDescriptor : IJsonTypeDescriptor +public interface ITypeDescriptor : ITypeDescriptor { public IValueAccessor Accessor { get; } public IFilterEvaluator FilterEvaluator { get; } diff --git a/src/Hyperbee.Json/Descriptors/IValueAccessor.cs b/src/Hyperbee.Json/Descriptors/IValueAccessor.cs index ce335a79..a3fe4c72 100644 --- a/src/Hyperbee.Json/Descriptors/IValueAccessor.cs +++ b/src/Hyperbee.Json/Descriptors/IValueAccessor.cs @@ -8,4 +8,5 @@ public interface IValueAccessor bool IsArray( in TNode value, out int length ); bool IsObject( in TNode value ); bool TryGetChildValue( in TNode value, string childKey, out TNode childValue ); + object GetAsValue( IEnumerable elements ); } diff --git a/src/Hyperbee.Json/Descriptors/Node/Functions/CountNodeFunction.cs b/src/Hyperbee.Json/Descriptors/Node/Functions/CountNodeFunction.cs index b053bd50..170f06cf 100644 --- a/src/Hyperbee.Json/Descriptors/Node/Functions/CountNodeFunction.cs +++ b/src/Hyperbee.Json/Descriptors/Node/Functions/CountNodeFunction.cs @@ -4,12 +4,12 @@ namespace Hyperbee.Json.Descriptors.Node.Functions; -public class CountNodeFunction( ParseExpressionContext context ) : FilterExtensionFunction( argumentCount: 1, context ) +public class CountNodeFunction() : FilterExtensionFunction( argumentCount: 1 ) { public const string Name = "count"; private static readonly Expression CountExpression = Expression.Constant( (Func, float>) Count ); - public override Expression GetExtensionExpression( Expression[] arguments, ParseExpressionContext context ) + protected override Expression GetExtensionExpression( Expression[] arguments ) { return Expression.Invoke( CountExpression, arguments[0] ); } diff --git a/src/Hyperbee.Json/Descriptors/Node/Functions/LengthNodeFunction.cs b/src/Hyperbee.Json/Descriptors/Node/Functions/LengthNodeFunction.cs index eec31a08..76341163 100644 --- a/src/Hyperbee.Json/Descriptors/Node/Functions/LengthNodeFunction.cs +++ b/src/Hyperbee.Json/Descriptors/Node/Functions/LengthNodeFunction.cs @@ -5,12 +5,12 @@ namespace Hyperbee.Json.Descriptors.Node.Functions; -public class LengthNodeFunction( ParseExpressionContext context ) : FilterExtensionFunction( argumentCount: 1, context ) +public class LengthNodeFunction() : FilterExtensionFunction( argumentCount: 1 ) { public const string Name = "length"; private static readonly Expression LengthExpression = Expression.Constant( (Func, float>) Length ); - public override Expression GetExtensionExpression( Expression[] arguments, ParseExpressionContext context ) + protected override Expression GetExtensionExpression( Expression[] arguments ) { return Expression.Invoke( LengthExpression, arguments[0] ); } diff --git a/src/Hyperbee.Json/Descriptors/Node/Functions/MatchNodeFunction.cs b/src/Hyperbee.Json/Descriptors/Node/Functions/MatchNodeFunction.cs index e32700db..6b259720 100644 --- a/src/Hyperbee.Json/Descriptors/Node/Functions/MatchNodeFunction.cs +++ b/src/Hyperbee.Json/Descriptors/Node/Functions/MatchNodeFunction.cs @@ -5,27 +5,26 @@ namespace Hyperbee.Json.Descriptors.Node.Functions; -public class MatchNodeFunction( ParseExpressionContext context ) : FilterExtensionFunction( argumentCount: 2, context ) +public class MatchNodeFunction() : FilterExtensionFunction( argumentCount: 2 ) { public const string Name = "match"; private static readonly Expression MatchExpression = Expression.Constant( (Func, string, bool>) Match ); - public override Expression GetExtensionExpression( Expression[] arguments, ParseExpressionContext context ) + protected override Expression GetExtensionExpression( Expression[] arguments ) { return Expression.Invoke( MatchExpression, arguments[0], arguments[1] ); } public static bool Match( IEnumerable nodes, string regex ) { - var nodeValue = nodes.FirstOrDefault()?.GetValue(); - if ( nodeValue == null ) + var value = nodes.FirstOrDefault()?.GetValue(); + + if ( value == null ) { return false; } var regexPattern = new Regex( regex.Trim( '\"', '\'' ) ); - var value = $"^{nodeValue}$"; - - return regexPattern.IsMatch( value ); + return regexPattern.IsMatch( $"^{value}$" ); } } diff --git a/src/Hyperbee.Json/Descriptors/Node/Functions/SearchNodeFunction.cs b/src/Hyperbee.Json/Descriptors/Node/Functions/SearchNodeFunction.cs index 39aadbf3..8f1f4e1a 100644 --- a/src/Hyperbee.Json/Descriptors/Node/Functions/SearchNodeFunction.cs +++ b/src/Hyperbee.Json/Descriptors/Node/Functions/SearchNodeFunction.cs @@ -5,25 +5,26 @@ namespace Hyperbee.Json.Descriptors.Node.Functions; -public class SearchNodeFunction( ParseExpressionContext context ) : FilterExtensionFunction( argumentCount: 2, context ) +public class SearchNodeFunction() : FilterExtensionFunction( argumentCount: 2 ) { public const string Name = "search"; private static readonly Expression SearchExpression = Expression.Constant( (Func, string, bool>) Search ); - public override Expression GetExtensionExpression( Expression[] arguments, ParseExpressionContext context ) + protected override Expression GetExtensionExpression( Expression[] arguments ) { return Expression.Invoke( SearchExpression, arguments[0], arguments[1] ); } public static bool Search( IEnumerable nodes, string regex ) { - var nodeValue = nodes.FirstOrDefault()?.GetValue(); - if ( nodeValue == null ) + var value = nodes.FirstOrDefault()?.GetValue(); + + if ( value == null ) { return false; } var regexPattern = new Regex( regex.Trim( '\"', '\'' ) ); - return regexPattern.IsMatch( nodeValue ); + return regexPattern.IsMatch( value ); } } diff --git a/src/Hyperbee.Json/Descriptors/Node/Functions/SelectNodeFunction.cs b/src/Hyperbee.Json/Descriptors/Node/Functions/SelectNodeFunction.cs deleted file mode 100644 index fb6f804c..00000000 --- a/src/Hyperbee.Json/Descriptors/Node/Functions/SelectNodeFunction.cs +++ /dev/null @@ -1,22 +0,0 @@ -using System.Linq.Expressions; -using System.Text.Json.Nodes; -using Hyperbee.Json.Filters.Parser; - -namespace Hyperbee.Json.Descriptors.Node.Functions; - -public class SelectNodeFunction( ParseExpressionContext context ) : FilterFunction -{ - private static readonly Expression SelectExpression = Expression.Constant( (Func>) Select ); - - protected override Expression GetExpressionImpl( ReadOnlySpan data, ReadOnlySpan item, ref int start, ref int from ) - { - var queryExp = Expression.Constant( item.ToString() ); - - return Expression.Invoke( SelectExpression, context.Current, context.Root, queryExp ); - } - - public static IEnumerable Select( JsonNode current, JsonNode root, string query ) - { - return JsonPath.Select( current, root, query ); - } -} diff --git a/src/Hyperbee.Json/Descriptors/Node/Functions/ValueNodeFunction.cs b/src/Hyperbee.Json/Descriptors/Node/Functions/ValueNodeFunction.cs index 87c711b9..f31f8fe4 100644 --- a/src/Hyperbee.Json/Descriptors/Node/Functions/ValueNodeFunction.cs +++ b/src/Hyperbee.Json/Descriptors/Node/Functions/ValueNodeFunction.cs @@ -6,13 +6,12 @@ namespace Hyperbee.Json.Descriptors.Node.Functions; -public class ValueNodeFunction( ParseExpressionContext context ) : FilterExtensionFunction( argumentCount: 1, context ) +public class ValueNodeFunction() : FilterExtensionFunction( argumentCount: 1 ) { public const string Name = "value"; public static readonly Expression ValueExpression = Expression.Constant( (Func, object>) Value ); - - public override Expression GetExtensionExpression( Expression[] arguments, ParseExpressionContext context ) + protected override Expression GetExtensionExpression( Expression[] arguments ) { return Expression.Invoke( ValueExpression, arguments[0] ); } @@ -33,15 +32,16 @@ public static object Value( IEnumerable nodes ) JsonValueKind.Undefined => false, _ => false }; - } - private static bool IsNotEmpty( JsonNode node ) - { - return node.GetValueKind() switch + static bool IsNotEmpty( JsonNode node ) { - JsonValueKind.Array => node.AsArray().Count != 0, - JsonValueKind.Object => node.AsObject().Count != 0, - _ => false - }; + return node.GetValueKind() switch + { + JsonValueKind.Array => node.AsArray().Count != 0, + JsonValueKind.Object => node.AsObject().Count != 0, + _ => false + }; + } } + } diff --git a/src/Hyperbee.Json/Descriptors/Node/NodeTypeDescriptor.cs b/src/Hyperbee.Json/Descriptors/Node/NodeTypeDescriptor.cs index 75d4d281..d73674ee 100644 --- a/src/Hyperbee.Json/Descriptors/Node/NodeTypeDescriptor.cs +++ b/src/Hyperbee.Json/Descriptors/Node/NodeTypeDescriptor.cs @@ -1,8 +1,6 @@ -using System.Linq.Expressions; -using System.Text.Json.Nodes; +using System.Text.Json.Nodes; using Hyperbee.Json.Descriptors.Node.Functions; using Hyperbee.Json.Filters; -using Hyperbee.Json.Filters.Parser; namespace Hyperbee.Json.Descriptors.Node; @@ -10,39 +8,21 @@ public class NodeTypeDescriptor : ITypeDescriptor { private FilterEvaluator _evaluator; private NodeValueAccessor _accessor; - public Dictionary Functions { get; init; } - public IValueAccessor Accessor - { - get => _accessor ??= new NodeValueAccessor(); - } - - public IFilterEvaluator FilterEvaluator - { - get => _evaluator ??= new FilterEvaluator( this ); - } - - public FilterFunction GetSelectFunction( ParseExpressionContext context ) => - new SelectNodeFunction( context ); + public FunctionRegistry Functions { get; } = new(); - public Expression GetValueExpression( Expression expression ) - { - if ( expression is null ) return null; + public IValueAccessor Accessor => + _accessor ??= new NodeValueAccessor(); - return expression.Type == typeof( IEnumerable ) - ? Expression.Invoke( ValueNodeFunction.ValueExpression, expression ) //Expression.Call( ValueNodeFunction.ValueMethod, expression ) - : expression; - } + public IFilterEvaluator FilterEvaluator => + _evaluator ??= new FilterEvaluator( this ); public NodeTypeDescriptor() { - Functions = new Dictionary( - [ - new KeyValuePair( CountNodeFunction.Name, context => new CountNodeFunction( context ) ), - new KeyValuePair( LengthNodeFunction.Name, context => new LengthNodeFunction( context ) ), - new KeyValuePair( MatchNodeFunction.Name, context => new MatchNodeFunction( context ) ), - new KeyValuePair( SearchNodeFunction.Name, context => new SearchNodeFunction( context ) ), - new KeyValuePair( ValueNodeFunction.Name, context => new ValueNodeFunction( context ) ), - ] ); + Functions.Register( CountNodeFunction.Name, () => new CountNodeFunction() ); + Functions.Register( LengthNodeFunction.Name, () => new LengthNodeFunction() ); + Functions.Register( MatchNodeFunction.Name, () => new MatchNodeFunction() ); + Functions.Register( SearchNodeFunction.Name, () => new SearchNodeFunction() ); + Functions.Register( ValueNodeFunction.Name, () => new ValueNodeFunction() ); } } diff --git a/src/Hyperbee.Json/Descriptors/Node/NodeValueAccessor.cs b/src/Hyperbee.Json/Descriptors/Node/NodeValueAccessor.cs index e8431ed0..907a5770 100644 --- a/src/Hyperbee.Json/Descriptors/Node/NodeValueAccessor.cs +++ b/src/Hyperbee.Json/Descriptors/Node/NodeValueAccessor.cs @@ -1,6 +1,9 @@ using System.Globalization; using System.Runtime.CompilerServices; +using System.Text.Json; using System.Text.Json.Nodes; +using Hyperbee.Json.Descriptors.Node.Functions; +using Hyperbee.Json.Extensions; namespace Hyperbee.Json.Descriptors.Node; @@ -117,4 +120,9 @@ static bool IsPathOperator( ReadOnlySpan x ) }; } } + + public object GetAsValue( IEnumerable nodes ) + { + return ValueNodeFunction.Value( nodes ); + } } diff --git a/src/Hyperbee.Json/Extensions/JsonPropertyFromPathExtensions.cs b/src/Hyperbee.Json/Extensions/JsonPropertyFromPathExtensions.cs index c208c65d..a9a9b69f 100644 --- a/src/Hyperbee.Json/Extensions/JsonPropertyFromPathExtensions.cs +++ b/src/Hyperbee.Json/Extensions/JsonPropertyFromPathExtensions.cs @@ -3,8 +3,8 @@ namespace Hyperbee.Json.Extensions; -// DISTINCT from JsonPath these extensions are intended to facilitate 'diving' for Json Properties by key. -// similar to JsonPointer but uses JsonPath notation. +// DISTINCT from JsonPath these extensions are intended to facilitate 'diving' for Json Properties using +// absolute singular paths. similar to JsonPointer but uses JsonPath notation. // // syntax supports singular paths; dotted notation, quoted names, and simple bracketed array accessors only. // diff --git a/src/Hyperbee.Json/Filters/FilterEvaluator.cs b/src/Hyperbee.Json/Filters/FilterEvaluator.cs index df4dcf06..dc089bfe 100644 --- a/src/Hyperbee.Json/Filters/FilterEvaluator.cs +++ b/src/Hyperbee.Json/Filters/FilterEvaluator.cs @@ -7,11 +7,10 @@ namespace Hyperbee.Json.Filters; public sealed class FilterEvaluator : IFilterEvaluator { - private readonly IJsonTypeDescriptor _typeDescriptor; - - // ReSharper disable once StaticMemberInGenericType private static readonly ConcurrentDictionary> Compiled = new(); + private readonly ITypeDescriptor _typeDescriptor; + public FilterEvaluator( ITypeDescriptor typeDescriptor ) { _typeDescriptor = typeDescriptor; @@ -19,7 +18,7 @@ public FilterEvaluator( ITypeDescriptor typeDescriptor ) public object Evaluate( string filter, TNode current, TNode root ) { - var compiled = Compiled.GetOrAdd( filter, _ => FilterExpressionParser.Compile( filter, _typeDescriptor ) ); + var compiled = Compiled.GetOrAdd( filter, _ => FilterParser.Compile( filter, _typeDescriptor ) ); try { diff --git a/src/Hyperbee.Json/Filters/Parser/Expressions/FunctionExpressionFactory.cs b/src/Hyperbee.Json/Filters/Parser/Expressions/FunctionExpressionFactory.cs new file mode 100644 index 00000000..9b927088 --- /dev/null +++ b/src/Hyperbee.Json/Filters/Parser/Expressions/FunctionExpressionFactory.cs @@ -0,0 +1,20 @@ +using System.Linq.Expressions; + +namespace Hyperbee.Json.Filters.Parser.Expressions; + +internal class FunctionExpressionFactory : IExpressionFactory +{ + public static bool TryGetExpression( ref ParserState state, out Expression expression, FilterContext context ) + { + if ( context.Descriptor.Functions.TryGetCreator( state.Item.ToString(), out var functionCreator ) ) + { + expression = functionCreator() + .GetExpression( ref state, context ); // will recurse for each function argument. + + return true; + } + + expression = null; + return false; + } +} diff --git a/src/Hyperbee.Json/Filters/Parser/Expressions/IExpressionFactory.cs b/src/Hyperbee.Json/Filters/Parser/Expressions/IExpressionFactory.cs new file mode 100644 index 00000000..6f13ac97 --- /dev/null +++ b/src/Hyperbee.Json/Filters/Parser/Expressions/IExpressionFactory.cs @@ -0,0 +1,8 @@ +using System.Linq.Expressions; + +namespace Hyperbee.Json.Filters.Parser.Expressions; + +internal interface IExpressionFactory +{ + static abstract bool TryGetExpression( ref ParserState state, out Expression expression, FilterContext context ); +} diff --git a/src/Hyperbee.Json/Filters/Parser/Expressions/LiteralExpressionFactory.cs b/src/Hyperbee.Json/Filters/Parser/Expressions/LiteralExpressionFactory.cs new file mode 100644 index 00000000..c4d9873e --- /dev/null +++ b/src/Hyperbee.Json/Filters/Parser/Expressions/LiteralExpressionFactory.cs @@ -0,0 +1,39 @@ +using System.Linq.Expressions; + +namespace Hyperbee.Json.Filters.Parser.Expressions; + +internal class LiteralExpressionFactory : IExpressionFactory +{ + public static bool TryGetExpression( ref ParserState state, out Expression expression, FilterContext context ) + { + expression = GetLiteralExpression( state.Item ); + return expression != null; + } + + private static ConstantExpression GetLiteralExpression( ReadOnlySpan item ) + { + // Check for known literals (true, false, null) first + + if ( item.Equals( "true", StringComparison.OrdinalIgnoreCase ) ) + return Expression.Constant( true ); + + if ( item.Equals( "false", StringComparison.OrdinalIgnoreCase ) ) + return Expression.Constant( false ); + + if ( item.Equals( "null", StringComparison.OrdinalIgnoreCase ) ) + return Expression.Constant( null ); + + // Check for quoted strings + + if ( item.Length >= 2 && (item[0] == '"' && item[^1] == '"' || item[0] == '\'' && item[^1] == '\'') ) + return Expression.Constant( item[1..^1].ToString() ); // remove quotes + + // Check for numbers + // TODO: Currently assuming all numbers are floats since we don't know what's in the data or the other side of the operator yet. + + if ( float.TryParse( item, out float result ) ) + return Expression.Constant( result ); + + return null; + } +} diff --git a/src/Hyperbee.Json/Filters/Parser/Expressions/NotExpressionFactory.cs b/src/Hyperbee.Json/Filters/Parser/Expressions/NotExpressionFactory.cs new file mode 100644 index 00000000..55afe51f --- /dev/null +++ b/src/Hyperbee.Json/Filters/Parser/Expressions/NotExpressionFactory.cs @@ -0,0 +1,12 @@ +using System.Linq.Expressions; + +namespace Hyperbee.Json.Filters.Parser.Expressions; + +internal class NotExpressionFactory : IExpressionFactory +{ + public static bool TryGetExpression( ref ParserState state, out Expression expression, FilterContext context ) + { + expression = null; + return state.Operator == Operator.Not; + } +} diff --git a/src/Hyperbee.Json/Filters/Parser/Expressions/ParenExpressionFactory.cs b/src/Hyperbee.Json/Filters/Parser/Expressions/ParenExpressionFactory.cs new file mode 100644 index 00000000..b6f8de67 --- /dev/null +++ b/src/Hyperbee.Json/Filters/Parser/Expressions/ParenExpressionFactory.cs @@ -0,0 +1,19 @@ +using System.Linq.Expressions; + +namespace Hyperbee.Json.Filters.Parser.Expressions; + +internal class ParenExpressionFactory : IExpressionFactory +{ + public static bool TryGetExpression( ref ParserState state, out Expression expression, FilterContext context ) + { + if ( state.Operator == Operator.OpenParen && state.Item.IsEmpty ) + { + var localState = state with { Terminal = FilterParser.EndArg }; + expression = FilterParser.Parse( ref localState, context ); // will recurse. + return true; + } + + expression = null; + return false; + } +} diff --git a/src/Hyperbee.Json/Filters/Parser/Expressions/SelectExpressionFactory.cs b/src/Hyperbee.Json/Filters/Parser/Expressions/SelectExpressionFactory.cs new file mode 100644 index 00000000..ac17d6d6 --- /dev/null +++ b/src/Hyperbee.Json/Filters/Parser/Expressions/SelectExpressionFactory.cs @@ -0,0 +1,35 @@ +using System.Linq.Expressions; + +namespace Hyperbee.Json.Filters.Parser.Expressions; + +internal class SelectExpressionFactory : IExpressionFactory +{ + public static bool TryGetExpression( ref ParserState state, out Expression expression, FilterContext context ) + { + expression = ExpressionHelper.GetExpression( state.Item, context ); + return expression != null; + } + + static class ExpressionHelper + { + private static readonly Expression SelectExpression = Expression.Constant( (Func>) Select ); + + public static Expression GetExpression( ReadOnlySpan item, FilterContext context ) + { + if ( item[0] != '$' && item[0] != '@' ) + return null; + + var queryExp = Expression.Constant( item.ToString() ); + + if ( item[0] == '$' ) // Current becomes root + context = context with { Current = context.Root }; + + return Expression.Invoke( SelectExpression, context.Current, context.Root, queryExp ); + } + + private static IEnumerable Select( TNode current, TNode root, string query ) + { + return JsonPath.SelectInternal( current, root, query ); + } + } +} diff --git a/src/Hyperbee.Json/Filters/Parser/FilterContext.cs b/src/Hyperbee.Json/Filters/Parser/FilterContext.cs new file mode 100644 index 00000000..e7d4ca8f --- /dev/null +++ b/src/Hyperbee.Json/Filters/Parser/FilterContext.cs @@ -0,0 +1,16 @@ +using System.Linq.Expressions; +using Hyperbee.Json.Descriptors; + +namespace Hyperbee.Json.Filters.Parser; + +internal record FilterContext +{ + public FilterContext( ITypeDescriptor descriptor ) + { + Descriptor = descriptor; + } + + public ParameterExpression Current { get; init; } = Expression.Parameter( typeof( TNode ), "current" ); + public ParameterExpression Root { get; } = Expression.Parameter( typeof( TNode ), "root" ); + public ITypeDescriptor Descriptor { get; } +} diff --git a/src/Hyperbee.Json/Filters/Parser/FilterExpressionParser.cs b/src/Hyperbee.Json/Filters/Parser/FilterExpressionParser.cs deleted file mode 100644 index f52b1e85..00000000 --- a/src/Hyperbee.Json/Filters/Parser/FilterExpressionParser.cs +++ /dev/null @@ -1,363 +0,0 @@ -#region License - -// This code is adapted from an algorithm published in MSDN Magazine, October 2015. -// Original article: "A Split-and-Merge Expression Parser in C#" by Vassili Kaplan. -// URL: https://learn.microsoft.com/en-us/archive/msdn-magazine/2015/october/csharp-a-split-and-merge-expression-parser-in-csharp -// -// Adapted for use in this project under the terms of the Microsoft Public License (Ms-PL). -// https://opensource.org/license/ms-pl-html - -#endregion - -using System.Linq.Expressions; -using System.Reflection; -using Hyperbee.Json.Descriptors; - -namespace Hyperbee.Json.Filters.Parser; - -public class FilterExpressionParser -{ - public const char EndLine = '\n'; - public const char EndArg = ')'; - public const char ArgSeparator = ','; - - private static readonly MethodInfo ObjectEquals = typeof( object ).GetMethod( "Equals", [typeof( object ), typeof( object )] ); - - public static Func Compile( ReadOnlySpan filter, IJsonTypeDescriptor typeDescriptor ) - { - var currentParam = Expression.Parameter( typeof( TNode ) ); - var rootParam = Expression.Parameter( typeof( TNode ) ); - var expressionContext = new ParseExpressionContext( currentParam, rootParam, typeDescriptor ); - var expression = Parse( filter, expressionContext ); - - return Expression - .Lambda>( expression, currentParam, rootParam ) - .Compile(); - } - - 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 FilterTruthyExpression.IsTruthyExpression( expression ); - } - - 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 ) - { - throw new ArgumentException( "Invalid filter", nameof( filter ) ); - } - - var tokens = new List(); - ReadOnlySpan currentPath = null; - char? quote = null; - - do - { - Next( filter, ref start, ref from, ref quote, out var result ); - - var (ch, type) = result; - - // special handling for "!" - if ( type == FilterTokenType.Not ) - { - tokens.Add( new FilterToken( null, type!.Value ) ); - continue; - } - - if ( StillCollecting( currentPath, ch, type, to ) ) - { - currentPath = filter[start..from].Trim(); - - if ( from < filter.Length && filter[from] != to ) - continue; - } - - start = from; - - // `GetExpression` may call recursively call `Parse` for nested expressions - var func = new FilterFunction( currentPath, type, context ); - var expression = func.GetExpression( filter, currentPath, ref start, ref from ); - - var filterType = ValidType( type ) - ? type!.Value - : UpdateType( filter, ref start, ref from, type, to ); - - tokens.Add( new FilterToken( expression, filterType ) ); - - currentPath = null; - - } while ( from < filter.Length && filter[from] != to ); - - if ( from < filter.Length && (filter[from] == EndArg || filter[from] == to) ) - { - // This happens when called recursively: move one char forward. - from++; - start = from; - } - - var baseToken = tokens[0]; - var index = 1; - - return Merge( baseToken, ref index, tokens, context ); - } - - private static void Next( ReadOnlySpan data, ref int start, ref int from, ref char? quote, out (char NextChar, FilterTokenType? Type) result ) - { - var nextChar = data[from++]; - switch ( nextChar ) - { - case '&' when ValidNextCharacter( data, from, '&' ): - from++; - start = from; - result = (nextChar, FilterTokenType.And); - break; - case '|' when ValidNextCharacter( data, from, '|' ): - from++; - start = from; - result = (nextChar, FilterTokenType.Or); - break; - case '=' when ValidNextCharacter( data, from, '=' ): - from++; - start = from; - result = (nextChar, FilterTokenType.Equals); - break; - case '!' when ValidNextCharacter( data, from, '=' ): - from++; - start = from; - result = (nextChar, FilterTokenType.NotEquals); - break; - case '>' when ValidNextCharacter( data, from, '=' ): - from++; - start = from; - result = (nextChar, FilterTokenType.GreaterThanOrEqual); - break; - case '<' when ValidNextCharacter( data, from, '=' ): - from++; - start = from; - result = (nextChar, FilterTokenType.LessThanOrEqual); - break; - case '>': - start = from; - result = (nextChar, FilterTokenType.GreaterThan); - break; - case '<': - start = from; - result = (nextChar, FilterTokenType.LessThan); - break; - case '!': - start = from; - result = (nextChar, FilterTokenType.Not); - break; - case '(': - result = (nextChar, FilterTokenType.OpenParen); - break; - case ')': - result = (nextChar, FilterTokenType.ClosedParen); - break; - case ' ' or '\t' when quote == null: - result = (nextChar, null); - break; - case '\'' or '\"' when from > 0 && data[from - 1] != '\\': - quote = quote == null ? nextChar : null; - result = (nextChar, null); - break; - default: - result = (nextChar, null); - break; - } - } - - private static bool ValidNextCharacter( ReadOnlySpan data, int from, char expected ) - { - return from < data.Length && data[from] == expected; - } - - private static bool StillCollecting( ReadOnlySpan item, char ch, FilterTokenType? type, char to ) - { - var stopCollecting = to is EndArg or EndLine - ? EndArg - : to; - - if ( item.Length == 0 && ch == EndArg ) - return true; - - if ( ValidType( type ) || ch == stopCollecting ) - return false; - - return type != FilterTokenType.OpenParen; - } - - private static bool ValidType( FilterTokenType? type ) - { - return type is FilterTokenType.Not or - FilterTokenType.Equals or - FilterTokenType.NotEquals or - FilterTokenType.GreaterThanOrEqual or - FilterTokenType.GreaterThan or - FilterTokenType.LessThanOrEqual or - FilterTokenType.LessThan or - FilterTokenType.Or or - FilterTokenType.And; - } - - private static FilterTokenType UpdateType( ReadOnlySpan item, ref int start, ref int from, FilterTokenType? type, char to ) - { - var startType = type; - - if ( from >= item.Length || item[from] == EndArg || item[from] == to ) - return FilterTokenType.ClosedParen; - - var index = from; - char? quote = null; - - while ( !ValidType( startType ) && index < item.Length ) - { - Next( item, ref start, ref index, ref quote, out var result ); - startType = result.Type; - } - - from = ValidType( startType ) ? index - : index > from ? index - 1 - : from; - - return startType!.Value; - } - - private static Expression Merge( FilterToken current, ref int index, List listToMerge, ParseExpressionContext context, bool mergeOneOnly = false ) - { - while ( index < listToMerge.Count ) - { - var next = listToMerge[index++]; - - while ( !CanMergeTokens( current, next ) ) - { - Merge( next, ref index, listToMerge, context, mergeOneOnly: true ); - } - - MergeTokens( current, next, context ); - - if ( mergeOneOnly ) - { - return current.Expression; - } - } - return current.Expression; - } - - private static bool CanMergeTokens( FilterToken left, FilterToken right ) - { - // "Not" can never be a right side operator - return right.Type != FilterTokenType.Not && GetPriority( left.Type ) >= GetPriority( right.Type ); - } - - private static int GetPriority( FilterTokenType type ) - { - return type switch - { - FilterTokenType.Not => 1, - FilterTokenType.And or FilterTokenType.Or => 2, - FilterTokenType.Equals or FilterTokenType.NotEquals or FilterTokenType.GreaterThan or FilterTokenType.GreaterThanOrEqual or FilterTokenType.LessThan or FilterTokenType.LessThanOrEqual => 3, - _ => 0, - }; - } - - private static void MergeTokens( FilterToken left, FilterToken right, ParseExpressionContext context ) - { - // Ensure both expressions are value expressions - left.Expression = context.Descriptor.GetValueExpression( left.Expression ); - right.Expression = context.Descriptor.GetValueExpression( right.Expression ); - - // Determine if we are comparing numerical values so that we can use the correct comparison method - bool isNumerical = IsNumerical( left.Expression?.Type ) || IsNumerical( right.Expression.Type ); - - left.Expression = left.Type switch - { - FilterTokenType.Equals => CompareConvert( isNumerical ? Expression.Equal : Equal, left.Expression, right.Expression, isNumerical ), - FilterTokenType.NotEquals => CompareConvert( isNumerical ? Expression.NotEqual : NotEqual, left.Expression, right.Expression, isNumerical ), - - // Assume/force numerical - FilterTokenType.GreaterThan => CompareConvert( Expression.GreaterThan, left.Expression, right.Expression ), - FilterTokenType.GreaterThanOrEqual => CompareConvert( Expression.GreaterThanOrEqual, left.Expression, right.Expression ), - FilterTokenType.LessThan => CompareConvert( Expression.LessThan, left.Expression, right.Expression ), - FilterTokenType.LessThanOrEqual => CompareConvert( Expression.LessThanOrEqual, left.Expression, right.Expression ), - - FilterTokenType.And => Expression.AndAlso( left.Expression!, right.Expression ), - FilterTokenType.Or => Expression.OrElse( left.Expression!, right.Expression ), - - FilterTokenType.Not => Expression.Not( right.Expression ), - _ => left.Expression - }; - - // Wrap left expression in a try-catch block to handle exceptions - left.Expression = left.Expression == null - ? left.Expression - : Expression.TryCatchFinally( - left.Expression, - Expression.Empty(), // Ensure finally block is present - Expression.Catch( typeof( Exception ), Expression.Constant( false ) ) - ); - - left.Type = right.Type; - return; - - // Use Equal Method vs equal operator - static Expression Equal( Expression l, Expression r ) => Expression.Call( ObjectEquals, l, r ); - static Expression NotEqual( Expression l, Expression r ) => Expression.Not( Equal( l, r ) ); - } - - private static Expression CompareConvert( Func compare, Expression left, Expression right, bool isNumerical = true ) - { - if ( isNumerical ) - { - if ( left.Type == typeof( object ) ) - left = Expression.Convert( left, typeof( float ) ); - - if ( right.Type == typeof( object ) ) - right = Expression.Convert( right, typeof( float ) ); - - if ( left.Type == typeof( int ) ) - left = Expression.Convert( left, typeof( float ) ); - - if ( right.Type == typeof( int ) ) - right = Expression.Convert( right, typeof( float ) ); - } - - if ( left.Type == typeof( object ) && right.Type == typeof( string ) ) - return compare( Expression.Convert( left, typeof( string ) ), right ); - - if ( left.Type == typeof( string ) && right.Type == typeof( object ) ) - return compare( left, Expression.Convert( right, typeof( string ) ) ); - - return compare( left, right ); - } - - private static bool IsNumerical( Type type ) - { - return type == typeof( int ) || type == typeof( float ); - } - - internal enum FilterTokenType - { - OpenParen, - ClosedParen, - Not, - Equals, - NotEquals, - LessThan, - LessThanOrEqual, - GreaterThan, - GreaterThanOrEqual, - Or, - And - } - - internal class FilterToken( Expression expression, FilterTokenType type ) - { - public Expression Expression { get; set; } = expression; - public FilterTokenType Type { get; set; } = type; - } -} diff --git a/src/Hyperbee.Json/Filters/Parser/FilterExtensionFunction.cs b/src/Hyperbee.Json/Filters/Parser/FilterExtensionFunction.cs index a0a63d8a..ab549e01 100644 --- a/src/Hyperbee.Json/Filters/Parser/FilterExtensionFunction.cs +++ b/src/Hyperbee.Json/Filters/Parser/FilterExtensionFunction.cs @@ -2,36 +2,37 @@ namespace Hyperbee.Json.Filters.Parser; -public abstract class FilterExtensionFunction : FilterFunction +public abstract class FilterExtensionFunction { private readonly int _argumentCount; - private readonly ParseExpressionContext _context; - protected FilterExtensionFunction( int argumentCount, ParseExpressionContext context ) + protected FilterExtensionFunction( int argumentCount ) { _argumentCount = argumentCount; - _context = context; } - public abstract Expression GetExtensionExpression( Expression[] arguments, ParseExpressionContext context ); + protected abstract Expression GetExtensionExpression( Expression[] arguments ); - protected override Expression GetExpressionImpl( ReadOnlySpan data, ReadOnlySpan item, ref int start, ref int from ) + internal Expression GetExpression( ref ParserState state, FilterContext context ) { var arguments = new Expression[_argumentCount]; for ( var i = 0; i < _argumentCount; i++ ) { - var argument = FilterExpressionParser.Parse( data, - ref start, - ref from, - i == _argumentCount - 1 - ? FilterExpressionParser.EndArg - : FilterExpressionParser.ArgSeparator, - _context ); + var localState = state with + { + Item = [], + Terminal = i == _argumentCount - 1 + ? FilterParser.EndArg + : FilterParser.ArgSeparator + }; + + var argument = FilterParser.Parse( ref localState, context ); arguments[i] = argument; } - return GetExtensionExpression( arguments, _context ); + return GetExtensionExpression( arguments ); } } + diff --git a/src/Hyperbee.Json/Filters/Parser/FilterFunction.cs b/src/Hyperbee.Json/Filters/Parser/FilterFunction.cs deleted file mode 100644 index 8ff1e0db..00000000 --- a/src/Hyperbee.Json/Filters/Parser/FilterFunction.cs +++ /dev/null @@ -1,83 +0,0 @@ -using System.Linq.Expressions; - -namespace Hyperbee.Json.Filters.Parser; - -public class FilterFunction -{ - private readonly FilterFunction _implementation; - - public FilterFunction() - { - _implementation = this; - } - - internal FilterFunction( ReadOnlySpan item, FilterExpressionParser.FilterTokenType? type, ParseExpressionContext context ) - { - if ( TryGetParenFunction( item, type, context, out _implementation ) ) - return; - - if ( TryGetFilterFunction( item, context, out _implementation ) ) - return; - - if ( TryGetExtensionFunction( item, context, out _implementation ) ) - return; - - // No functions not found, 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.GetExpressionImpl( data, item, ref start, ref from ); - } - - protected virtual Expression GetExpressionImpl( 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 TryGetParenFunction( ReadOnlySpan item, FilterExpressionParser.FilterTokenType? type, ParseExpressionContext context, out FilterFunction function ) - { - function = null; - - if ( item.Length != 0 || type != FilterExpressionParser.FilterTokenType.OpenParen ) - return false; - - function = new ParenFunction( context ); - return true; - } - - private static bool TryGetFilterFunction( ReadOnlySpan item, ParseExpressionContext context, out FilterFunction function ) - { - switch ( item[0] ) - { - case '@': - function = context.Descriptor.GetSelectFunction( context ); - return true; - case '$': - // Current becomes root - function = context.Descriptor.GetSelectFunction( context with { Current = context.Root } ); - return true; - } - - function = null; - return false; - } - - private static bool TryGetExtensionFunction( ReadOnlySpan item, ParseExpressionContext context, out FilterFunction function ) - { - function = null; - - var method = item.ToString(); - - if ( !context.Descriptor.Functions.TryGetValue( method, out var creator ) ) - return false; - - function = creator( context ); - return true; - - } -} diff --git a/src/Hyperbee.Json/Filters/Parser/FilterParser.cs b/src/Hyperbee.Json/Filters/Parser/FilterParser.cs new file mode 100644 index 00000000..cac31577 --- /dev/null +++ b/src/Hyperbee.Json/Filters/Parser/FilterParser.cs @@ -0,0 +1,438 @@ +#region License + +// This code is adapted from an algorithm published in MSDN Magazine, October 2015. +// Original article: "A Split-and-Merge Expression Parser in C#" by Vassili Kaplan. +// URL: https://learn.microsoft.com/en-us/archive/msdn-magazine/2015/october/csharp-a-split-and-merge-expression-parser-in-csharp +// +// Adapted for use in this project under the terms of the Microsoft Public License (Ms-PL). +// https://opensource.org/license/ms-pl-html + +#endregion + +using System.Linq.Expressions; +using System.Reflection; +using Hyperbee.Json.Descriptors; +using Hyperbee.Json.Filters.Parser.Expressions; + +namespace Hyperbee.Json.Filters.Parser; + +public abstract class FilterParser +{ + public const char EndLine = '\n'; + public const char EndArg = ')'; + public const char ArgSeparator = ','; + + protected static readonly MethodInfo ObjectEquals = typeof( object ).GetMethod( "Equals", [typeof( object ), typeof( object )] ); +} + +public class FilterParser : FilterParser +{ + public static Func Compile( ReadOnlySpan filter, ITypeDescriptor descriptor ) + { + var context = new FilterContext( descriptor ); + + var expression = Parse( filter, context ); + + return Expression.Lambda>( expression, context.Current, context.Root ).Compile(); + } + + internal static Expression Parse( ReadOnlySpan filter, FilterContext context ) + { + var pos = 0; + var state = new ParserState( filter, [], ref pos, Operator.Nop, EndLine ); + + var expression = Parse( ref state, context ); + + return FilterTruthyExpression.IsTruthyExpression( expression ); + } + + internal static Expression Parse( ref ParserState state, FilterContext context ) + { + // validate input + if ( context == null ) + throw new ArgumentNullException( nameof( context ) ); + + if ( state.EndOfBuffer || state.IsTerminal ) + throw new ArgumentException( $"Invalid filter: \"{state.Buffer}\"", nameof( state ) ); + + // parse the expression + var items = new List(); + + do + { + MoveNext( ref state ); + items.Add( GetExprItem( ref state, context ) ); + + } while ( state.IsParsing ); + + // advance to next character for recursive calls. + if ( !state.EndOfBuffer && state.IsTerminal ) + state.Pos++; + + // merge the expressions + var baseItem = items[0]; + var index = 1; + + return Merge( baseItem, ref index, items, context.Descriptor ); + } + + private static ExprItem GetExprItem( ref ParserState state, FilterContext context ) + { + if ( NotExpressionFactory.TryGetExpression( ref state, out var expression, context ) ) + return ExprItem( ref state, expression ); + + if ( ParenExpressionFactory.TryGetExpression( ref state, out expression, context ) ) // will recurse. + return ExprItem( ref state, expression ); + + if ( SelectExpressionFactory.TryGetExpression( ref state, out expression, context ) ) + return ExprItem( ref state, expression ); + + if ( FunctionExpressionFactory.TryGetExpression( ref state, out expression, context ) ) // may recurse for each function argument. + return ExprItem( ref state, expression ); + + if ( LiteralExpressionFactory.TryGetExpression( ref state, out expression, context ) ) + return ExprItem( ref state, expression ); + + throw new ArgumentException( $"Unsupported literal: {state.Buffer.ToString()}" ); + + // Helper method to create an expression item + static ExprItem ExprItem( ref ParserState state, Expression expression ) + { + UpdateOperator( ref state ); + return new ExprItem( expression, state.Operator ); + } + } + + private static void MoveNext( ref ParserState state ) + { + char? quote = null; + + // remove leading whitespace + while ( !state.EndOfBuffer && char.IsWhiteSpace( state.Current ) ) + state.Pos++; + + // check for end of buffer + if ( state.EndOfBuffer ) + { + state.Operator = Operator.Nop; + state.Item = []; + return; + } + + // read next item + var itemStart = state.Pos; + int itemEnd; + + while ( true ) + { + itemEnd = state.Pos; // assign before the call to NextCharacter + + NextCharacter( ref state, out var nextChar, ref quote ); + + if ( IsFinished( state.Pos - itemStart, nextChar, state.Operator, state.Terminal ) ) + break; + + if ( !state.EndOfBuffer && !state.IsTerminal ) + continue; + + itemEnd = state.Pos; // fall-through: include the terminal character + break; + } + + state.SetItem( itemStart, itemEnd ); + return; + + // Helper method to determine if item parsing is finished + static bool IsFinished( int count, char ch, Operator op, char terminal ) + { + // order of operations matters here + if ( count == 0 && ch == EndArg ) + return false; + + if ( op != Operator.Nop && op != Operator.ClosedParen ) + return true; + + if ( ch == terminal || ch == EndArg || ch == EndLine ) + return true; + + return false; + } + } + + private static void NextCharacter( ref ParserState state, out char nextChar, ref char? quoteChar ) + { + nextChar = state.Buffer[state.Pos++]; + + switch ( nextChar ) + { + case '&' when Next( ref state, '&' ): + state.Operator = Operator.And; + break; + case '|' when Next( ref state, '|' ): + state.Operator = Operator.Or; + break; + case '=' when Next( ref state, '=' ): + state.Operator = Operator.Equals; + break; + case '!' when Next( ref state, '=' ): + state.Operator = Operator.NotEquals; + break; + case '>' when Next( ref state, '=' ): + state.Operator = Operator.GreaterThanOrEqual; + break; + case '<' when Next( ref state, '=' ): + state.Operator = Operator.LessThanOrEqual; + break; + case '>': + state.Operator = Operator.GreaterThan; + break; + case '<': + state.Operator = Operator.LessThan; + break; + case '!': + state.Operator = Operator.Not; + break; + case '(': + state.Operator = Operator.OpenParen; + break; + case ')': + state.Operator = Operator.ClosedParen; + break; + case ' ' or '\t' when quoteChar == null: + state.Operator = Operator.Nop; + break; + case '\'' or '\"' when state.Pos > 0 && state.Previous != '\\': + quoteChar = quoteChar == null ? nextChar : null; + state.Operator = Operator.Nop; + break; + default: + state.Operator = Operator.Nop; + break; + } + + return; + + // Helper method to check if the next character is the expected character + static bool Next( ref ParserState state, char expected ) + { + if ( state.EndOfBuffer || state.Current != expected ) + return false; + + state.Pos++; + return true; + } + } + + private static void UpdateOperator( ref ParserState state ) + { + if ( !IsParenOrNop( state.Operator ) ) + return; + + if ( state.EndOfBuffer ) + { + state.Operator = Operator.Nop; + return; + } + + if ( state.IsTerminal ) + { + state.Operator = Operator.ClosedParen; + return; + } + + char? quoteChar = null; + var startPos = state.Pos; + + while ( IsParenOrNop( state.Operator ) && !state.EndOfBuffer ) + { + NextCharacter( ref state, out _, ref quoteChar ); + } + + if ( IsParen( state.Operator ) && state.Pos > startPos ) + { + state.Pos--; + } + + return; + + // Helper method to determine if an operator is a parenthesis or a no-op + static bool IsParenOrNop( Operator op ) => op is Operator.OpenParen or Operator.ClosedParen or Operator.Nop; + static bool IsParen( Operator op ) => op is Operator.OpenParen or Operator.ClosedParen; + } + + private static Expression Merge( ExprItem current, ref int index, List items, ITypeDescriptor descriptor, bool mergeOneOnly = false ) + { + while ( index < items.Count ) + { + var next = items[index++]; + + while ( !CanMergeItems( current, next ) ) + { + Merge( next, ref index, items, descriptor, mergeOneOnly: true ); // recursive call + } + + MergeItems( current, next, descriptor ); + + if ( mergeOneOnly ) + return current.Expression; + } + + return current.Expression; + + // Helper method to determine if two items can be merged + static bool CanMergeItems( ExprItem left, ExprItem right ) + { + // "Not" can never be a right side operator + return right.Operator != Operator.Not && GetPriority( left.Operator ) >= GetPriority( right.Operator ); + } + + // Helper method to get the priority of an operator + static int GetPriority( Operator type ) + { + return type switch + { + Operator.Not => 1, + Operator.And or + Operator.Or => 2, + Operator.Equals or + Operator.NotEquals or + Operator.GreaterThan or + Operator.GreaterThanOrEqual or + Operator.LessThan or + Operator.LessThanOrEqual => 3, + _ => 0, + }; + } + } + + private static void MergeItems( ExprItem left, ExprItem right, ITypeDescriptor descriptor ) + { + // Ensure both expressions are value expressions + left.Expression = ExpressionConverter.ConvertToValue( descriptor.Accessor, left.Expression ); + right.Expression = ExpressionConverter.ConvertToValue( descriptor.Accessor, right.Expression ); + + left.Expression = left.Operator switch + { + Operator.Equals when IsNumerical( left.Expression?.Type ) || IsNumerical( right.Expression.Type ) => + Expression.Equal( + ExpressionConverter.ConvertToNumber( left.Expression ), + ExpressionConverter.ConvertToNumber( right.Expression ) ), + + Operator.NotEquals when IsNumerical( left.Expression?.Type ) || IsNumerical( right.Expression.Type ) => + Expression.NotEqual( + ExpressionConverter.ConvertToNumber( left.Expression ), + ExpressionConverter.ConvertToNumber( right.Expression ) ), + + Operator.GreaterThan => + Expression.GreaterThan( + ExpressionConverter.ConvertToNumber( left.Expression ), + ExpressionConverter.ConvertToNumber( right.Expression ) ), + + Operator.GreaterThanOrEqual => + Expression.GreaterThanOrEqual( + ExpressionConverter.ConvertToNumber( left.Expression ), + ExpressionConverter.ConvertToNumber( right.Expression ) ), + + Operator.LessThan => + Expression.LessThan( + ExpressionConverter.ConvertToNumber( left.Expression ), + ExpressionConverter.ConvertToNumber( right.Expression ) ), + + Operator.LessThanOrEqual => + Expression.LessThanOrEqual( + ExpressionConverter.ConvertToNumber( left.Expression ), + ExpressionConverter.ConvertToNumber( right.Expression ) ), + + Operator.Equals => Equal( left.Expression, right.Expression ), + Operator.NotEquals => NotEqual( left.Expression, right.Expression ), + Operator.And => Expression.AndAlso( left.Expression!, right.Expression ), + Operator.Or => Expression.OrElse( left.Expression!, right.Expression ), + Operator.Not => Expression.Not( right.Expression ), + _ => left.Expression + }; + + // Wrap left expression in a try-catch block to handle exceptions + left.Expression = left.Expression == null + ? left.Expression + : Expression.TryCatchFinally( + left.Expression, + Expression.Empty(), // Ensure finally block is present + Expression.Catch( typeof( Exception ), Expression.Constant( false ) ) + ); + + left.Operator = right.Operator; + return; + + // Helper method to determine if a type is numerical + static bool IsNumerical( Type type ) => type == typeof( float ) || type == typeof( int ); + + // Helper methods to create comparison expressions + static Expression Equal( Expression l, Expression r ) => Expression.Call( FilterParser.ObjectEquals, l, r ); + static Expression NotEqual( Expression l, Expression r ) => Expression.Not( Equal( l, r ) ); + } + + private static class ExpressionConverter + { + // Cache the MethodInfo for GetAsValue method + private static readonly MethodInfo GetAsValueMethodInfo = typeof( IValueAccessor ) + .GetMethod( nameof( IValueAccessor.GetAsValue ) ); + + // Cache the compiled delegate for calling GetAsValue method + private static readonly Func, IEnumerable, object> GetAsValueDelegate; + + static ExpressionConverter() + { + // Create parameters for the expression + var accessorParam = Expression.Parameter( typeof( IValueAccessor ), "accessor" ); + var expressionParam = Expression.Parameter( typeof( IEnumerable ), "expression" ); + + // Create the expression to call the GetAsValue method + var callExpression = Expression.Call( accessorParam, GetAsValueMethodInfo, expressionParam ); + + // Compile the expression into a delegate + GetAsValueDelegate = Expression.Lambda, IEnumerable, object>>( + callExpression, accessorParam, expressionParam ).Compile(); + } + + public static Expression ConvertToValue( IValueAccessor accessor, Expression expression ) + { + if ( expression == null ) + return null; + + if ( expression.Type != typeof( IEnumerable ) ) + return expression; + + // Create an expression representing the instance of the descriptor + var accessorExpression = Expression.Constant( accessor ); + + // Use the compiled delegate to create an expression to call the GetAsValue method + return Expression.Invoke( Expression.Constant( GetAsValueDelegate ), accessorExpression, expression ); + } + + // Helper method to convert numerical types to float + public static Expression ConvertToNumber( Expression expression ) + { + if ( expression.Type == typeof( float ) ) // quick out + return expression; + + if ( expression.Type == typeof( object ) || + expression.Type == typeof( int ) || + expression.Type == typeof( short ) || + expression.Type == typeof( long ) || + expression.Type == typeof( double ) || + expression.Type == typeof( decimal ) ) + { + return Expression.Convert( expression, typeof( float ) ); + } + + return expression; + } + + } + + private class ExprItem( Expression expression, Operator op ) + { + public Expression Expression { get; set; } = expression; + public Operator Operator { get; set; } = op; + } +} diff --git a/src/Hyperbee.Json/Filters/Parser/FilterTruthyExpression.cs b/src/Hyperbee.Json/Filters/Parser/FilterTruthyExpression.cs index cf71bb15..3e543461 100644 --- a/src/Hyperbee.Json/Filters/Parser/FilterTruthyExpression.cs +++ b/src/Hyperbee.Json/Filters/Parser/FilterTruthyExpression.cs @@ -1,36 +1,29 @@ using System.Collections; -using System.Linq.Expressions; using System.Reflection; -namespace Hyperbee.Json.Filters.Parser; - -public static class FilterTruthyExpression +namespace Hyperbee.Json.Filters.Parser { - private static readonly MethodInfo IsTruthyMethod; - - static FilterTruthyExpression() + public static class FilterTruthyExpression { - IsTruthyMethod = typeof( FilterTruthyExpression ).GetMethod( nameof( IsTruthy ), [typeof( object )] ); - } + private static readonly MethodInfo IsTruthyMethodInfo = typeof( FilterTruthyExpression ).GetMethod( nameof( IsTruthy ) ); - public static Expression IsTruthyExpression( Expression expression ) => - expression.Type == typeof( bool ) - ? expression - : Expression.Call( IsTruthyMethod, expression ); + public static System.Linq.Expressions.Expression IsTruthyExpression( System.Linq.Expressions.Expression expression ) => + expression.Type == typeof( bool ) + ? expression + : System.Linq.Expressions.Expression.Call( IsTruthyMethodInfo, expression ); - public static bool IsTruthy( object obj ) => !IsFalsy( obj ); - - public static bool IsFalsy( object obj ) - { - return obj switch + public static bool IsTruthy( object value ) { - null => true, - bool boolValue => !boolValue, - string str => string.IsNullOrEmpty( str ) || str.Equals( "false", StringComparison.OrdinalIgnoreCase ), - Array array => array.Length == 0, - IEnumerable enumerable => !enumerable.Cast().Any(), - IConvertible convertible => !Convert.ToBoolean( convertible ), - _ => false - }; + return value switch + { + null => false, + bool boolValue => boolValue, + string str => !string.IsNullOrEmpty( str ) && !str.Equals( "false", StringComparison.OrdinalIgnoreCase ), + Array array => array.Length > 0, + IEnumerable enumerable => enumerable.Cast().Any(), + IConvertible convertible => Convert.ToBoolean( convertible ), + _ => true + }; + } } } diff --git a/src/Hyperbee.Json/Filters/Parser/LiteralFunction.cs b/src/Hyperbee.Json/Filters/Parser/LiteralFunction.cs deleted file mode 100644 index dbf9b715..00000000 --- a/src/Hyperbee.Json/Filters/Parser/LiteralFunction.cs +++ /dev/null @@ -1,48 +0,0 @@ -using System.Linq.Expressions; - -namespace Hyperbee.Json.Filters.Parser; - - -internal class LiteralFunction : FilterFunction -{ - protected override Expression GetExpressionImpl( ReadOnlySpan data, ReadOnlySpan item, ref int start, ref int from ) - { - // Check for known literals (true, false, null) first - - if ( item.Equals( "true", StringComparison.OrdinalIgnoreCase ) ) - return Expression.Constant( true ); - - if ( item.Equals( "false", StringComparison.OrdinalIgnoreCase ) ) - return Expression.Constant( false ); - - if ( item.Equals( "null", StringComparison.OrdinalIgnoreCase ) ) - return Expression.Constant( null ); - - // Check for quoted strings - - if ( TryRemoveQuotes( ref item ) ) - return Expression.Constant( item.ToString() ); - - // Check for numbers - // TODO: Currently assuming all numbers are floats since we don't know what's in the data or the other side of the operator yet. - - if ( float.TryParse( item, out float result ) ) - return Expression.Constant( result ); - - throw new ArgumentException( $"Unsupported literal: {item.ToString()}" ); - - static bool TryRemoveQuotes( ref ReadOnlySpan input ) - { - if ( !IsQuoted( input ) ) - return false; - - input = input[1..^1]; - return true; - - static bool IsQuoted( ReadOnlySpan input ) - { - return input.Length >= 2 && ((input[0] == '"' && input[^1] == '"') || (input[0] == '\'' && input[^1] == '\'')); - } - } - } -} diff --git a/src/Hyperbee.Json/Filters/Parser/Operator.cs b/src/Hyperbee.Json/Filters/Parser/Operator.cs new file mode 100644 index 00000000..14164f09 --- /dev/null +++ b/src/Hyperbee.Json/Filters/Parser/Operator.cs @@ -0,0 +1,17 @@ +namespace Hyperbee.Json.Filters.Parser; + +public enum Operator +{ + Nop = 0, // used to represent an unassigned token + OpenParen, + ClosedParen, + Not, + Equals, + NotEquals, + LessThan, + LessThanOrEqual, + GreaterThan, + GreaterThanOrEqual, + Or, + And +} diff --git a/src/Hyperbee.Json/Filters/Parser/ParenFunction.cs b/src/Hyperbee.Json/Filters/Parser/ParenFunction.cs deleted file mode 100644 index 0ae718af..00000000 --- a/src/Hyperbee.Json/Filters/Parser/ParenFunction.cs +++ /dev/null @@ -1,11 +0,0 @@ -using System.Linq.Expressions; - -namespace Hyperbee.Json.Filters.Parser; - -internal class ParenFunction( ParseExpressionContext context ) : FilterFunction -{ - protected override Expression GetExpressionImpl( ReadOnlySpan data, ReadOnlySpan item, ref int start, ref int from ) - { - return FilterExpressionParser.Parse( data, ref start, ref from, FilterExpressionParser.EndArg, context ); - } -} diff --git a/src/Hyperbee.Json/Filters/Parser/ParseExpressionContext.cs b/src/Hyperbee.Json/Filters/Parser/ParseExpressionContext.cs deleted file mode 100644 index de743c6e..00000000 --- a/src/Hyperbee.Json/Filters/Parser/ParseExpressionContext.cs +++ /dev/null @@ -1,10 +0,0 @@ -using System.Linq.Expressions; -using Hyperbee.Json.Descriptors; - -namespace Hyperbee.Json.Filters.Parser; - -public record ParseExpressionContext( - Expression Current, - Expression Root, - IJsonTypeDescriptor Descriptor -); diff --git a/src/Hyperbee.Json/Filters/Parser/ParserState.cs b/src/Hyperbee.Json/Filters/Parser/ParserState.cs new file mode 100644 index 00000000..e90d673f --- /dev/null +++ b/src/Hyperbee.Json/Filters/Parser/ParserState.cs @@ -0,0 +1,30 @@ +namespace Hyperbee.Json.Filters.Parser; + +public ref struct ParserState +{ + public ReadOnlySpan Buffer { get; } + public ReadOnlySpan Item { get; internal set; } + + public Operator Operator { get; set; } + public char Terminal { get; init; } + + public readonly ref int Pos; + + internal ParserState( ReadOnlySpan buffer, ReadOnlySpan item, ref int pos, Operator tokenType, char terminal ) + { + Buffer = buffer; + Item = item; + Operator = tokenType; + Terminal = terminal; + Pos = ref pos; + } + + public readonly bool EndOfBuffer => Pos >= Buffer.Length; + public readonly bool IsParsing => Pos < Buffer.Length && Buffer[Pos] != Terminal; + public readonly bool IsTerminal => Buffer[Pos] == Terminal; + + public readonly char Current => Buffer[Pos]; + public readonly char Previous => Buffer[Pos - 1]; + + internal void SetItem( int itemStart, int itemEnd ) => Item = Buffer[itemStart..itemEnd].TrimEnd(); +} diff --git a/src/Hyperbee.Json/Hyperbee.Json.csproj b/src/Hyperbee.Json/Hyperbee.Json.csproj index 4bb6bb6e..8a3c3e16 100644 --- a/src/Hyperbee.Json/Hyperbee.Json.csproj +++ b/src/Hyperbee.Json/Hyperbee.Json.csproj @@ -23,6 +23,9 @@ <_Parameter1>$(AssemblyName).Tests + + <_Parameter1>$(AssemblyName).Benchmark + diff --git a/src/Hyperbee.Json/JsonPath.cs b/src/Hyperbee.Json/JsonPath.cs index 8c8e2679..76ca5502 100644 --- a/src/Hyperbee.Json/JsonPath.cs +++ b/src/Hyperbee.Json/JsonPath.cs @@ -52,8 +52,11 @@ public static IEnumerable Select( in TNode value, string query ) return EnumerateMatches( value, value, query ); } - internal static IEnumerable Select( in TNode value, TNode root, string query ) + internal static IEnumerable SelectInternal( in TNode value, TNode root, string query ) { + // this overload is required for reentrant filter select evaluations. + // it is intended for use by nameof(FilterFunction) implementations. + return EnumerateMatches( value, root, query ); } diff --git a/src/Hyperbee.Json/JsonPathQueryParser.cs b/src/Hyperbee.Json/JsonPathQueryParser.cs index 8ac18955..0aed554d 100644 --- a/src/Hyperbee.Json/JsonPathQueryParser.cs +++ b/src/Hyperbee.Json/JsonPathQueryParser.cs @@ -2,8 +2,6 @@ using System.Text.RegularExpressions; 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 [Flags] public enum SelectorKind diff --git a/src/Hyperbee.Json/JsonPathBuilder.cs b/src/Hyperbee.Json/JsonPathResolver.cs similarity index 97% rename from src/Hyperbee.Json/JsonPathBuilder.cs rename to src/Hyperbee.Json/JsonPathResolver.cs index 66dbbf4a..809e0af3 100644 --- a/src/Hyperbee.Json/JsonPathBuilder.cs +++ b/src/Hyperbee.Json/JsonPathResolver.cs @@ -2,18 +2,18 @@ namespace Hyperbee.Json; -public class JsonPathBuilder +public class JsonPathResolver { private readonly JsonElement _rootElement; private readonly JsonElementPositionComparer _comparer = new(); private readonly Dictionary _parentMap = []; - public JsonPathBuilder( JsonDocument rootDocument ) + public JsonPathResolver( JsonDocument rootDocument ) : this( rootDocument.RootElement ) { } - public JsonPathBuilder( JsonElement rootElement ) + public JsonPathResolver( JsonElement rootElement ) { _rootElement = rootElement; diff --git a/src/Hyperbee.Json/JsonPathSegment.cs b/src/Hyperbee.Json/JsonPathSegment.cs index a03ab617..47d66b29 100644 --- a/src/Hyperbee.Json/JsonPathSegment.cs +++ b/src/Hyperbee.Json/JsonPathSegment.cs @@ -60,12 +60,6 @@ public IEnumerable AsEnumerable() } } - public void Deconstruct( out bool singular, out SelectorDescriptor[] selectors ) - { - singular = Singular; - selectors = Selectors; - } - private bool IsSingular() { if ( Selectors.Length != 1 ) @@ -74,6 +68,12 @@ private bool IsSingular() return (Selectors[0].SelectorKind & SelectorKind.Singular) == SelectorKind.Singular; } + public void Deconstruct( out bool singular, out SelectorDescriptor[] selectors ) + { + singular = Singular; + selectors = Selectors; + } + internal class SegmentDebugView( JsonPathSegment instance ) { [DebuggerBrowsable( DebuggerBrowsableState.RootHidden )] diff --git a/src/Hyperbee.Json/JsonTypeDescriptorRegistry.cs b/src/Hyperbee.Json/JsonTypeDescriptorRegistry.cs index 6d63ec50..9a989513 100644 --- a/src/Hyperbee.Json/JsonTypeDescriptorRegistry.cs +++ b/src/Hyperbee.Json/JsonTypeDescriptorRegistry.cs @@ -6,7 +6,7 @@ namespace Hyperbee.Json; public class JsonTypeDescriptorRegistry { - private static readonly Dictionary Descriptors = []; + private static readonly Dictionary Descriptors = []; static JsonTypeDescriptorRegistry() { diff --git a/test/Hyperbee.Json.Benchmark/FilterExpressionParserEvaluator.cs b/test/Hyperbee.Json.Benchmark/FilterExpressionParserEvaluator.cs index 894a1a10..fc81c635 100644 --- a/test/Hyperbee.Json.Benchmark/FilterExpressionParserEvaluator.cs +++ b/test/Hyperbee.Json.Benchmark/FilterExpressionParserEvaluator.cs @@ -2,6 +2,7 @@ using System.Text.Json; using System.Text.Json.Nodes; using BenchmarkDotNet.Attributes; +using Hyperbee.Json.Descriptors; using Hyperbee.Json.Descriptors.Element; using Hyperbee.Json.Descriptors.Node; using Hyperbee.Json.Filters.Parser; @@ -10,8 +11,8 @@ namespace Hyperbee.Json.Benchmark; public class FilterExpressionParserEvaluator { - private ParseExpressionContext _nodeExpressionContext; - private ParseExpressionContext _elementExpressionContext; + private FilterContext _nodeExecutionContext; + private FilterContext _elementExecutionContext; [Params( "(\"world\" == 'world') && (true || false)" )] public string Filter; @@ -19,26 +20,20 @@ public class FilterExpressionParserEvaluator [GlobalSetup] public void Setup() { - _nodeExpressionContext = new ParseExpressionContext( - Expression.Parameter( typeof( JsonNode ) ), - Expression.Parameter( typeof( JsonNode ) ), - new NodeTypeDescriptor() ); + _nodeExecutionContext = new FilterContext( new NodeTypeDescriptor() ); - _elementExpressionContext = new ParseExpressionContext( - Expression.Parameter( typeof( JsonElement ) ), - Expression.Parameter( typeof( JsonElement ) ), - new ElementTypeDescriptor() ); + _elementExecutionContext = new FilterContext( new ElementTypeDescriptor() ); } [Benchmark] public void JsonPathFilterParser_JsonElement() { - FilterExpressionParser.Parse( Filter, _elementExpressionContext ); + FilterParser.Parse( Filter, _elementExecutionContext ); } [Benchmark] public void JsonPathFilterParser_JsonNode() { - FilterExpressionParser.Parse( Filter, _nodeExpressionContext ); + FilterParser.Parse( Filter, _nodeExecutionContext ); } } diff --git a/test/Hyperbee.Json.Tests/Evaluators/FilterExpressionParserTests.cs b/test/Hyperbee.Json.Tests/Evaluators/FilterExpressionParserTests.cs index 70f3b27c..460402cf 100644 --- a/test/Hyperbee.Json.Tests/Evaluators/FilterExpressionParserTests.cs +++ b/test/Hyperbee.Json.Tests/Evaluators/FilterExpressionParserTests.cs @@ -125,6 +125,26 @@ public void Should_MatchExpectedResult_WhenUsingFunctions( string filter, bool e Assert.AreEqual( expected, result ); } + [DataTestMethod] + [DataRow( "length(@.store.book) == 4", true, typeof( JsonElement ) )] + [DataRow( "length(@.store.book) == 4 ", true, typeof( JsonElement ) )] + [DataRow( " length(@.store.book) == 4", true, typeof( JsonElement ) )] + [DataRow( " length(@.store.book) == 4 ", true, typeof( JsonElement ) )] + [DataRow( " length( @.store.book ) == 4 ", true, typeof( JsonElement ) )] + [DataRow( "4 == length(@.store.book)", true, typeof( JsonElement ) )] + [DataRow( "4 == length( @.store.book ) ", true, typeof( JsonElement ) )] + [DataRow( " 4 == length(@.store.book)", true, typeof( JsonElement ) )] + [DataRow( " 4 == length(@.store.book) ", true, typeof( JsonElement ) )] + [DataRow( " 4 == length( @.store.book ) ", true, typeof( JsonElement ) )] + public void Should_MatchExpectedResult_WhenHasExtraSpaces( string filter, bool expected, Type sourceType ) + { + // arrange & act + var result = CompileAndExecute( filter, sourceType ); + + // assert + Assert.AreEqual( expected, result ); + } + [DataTestMethod] [DataRow( "unknown_literal", typeof( JsonElement ) )] [DataRow( "'unbalanced string\"", typeof( JsonElement ) )] @@ -152,18 +172,14 @@ public void Should_FailToParse_WhenUsingInvalidFilters( string filter, Type sour private static (Expression, ParameterExpression) GetExpression( string filter, Type sourceType ) { - var param = Expression.Parameter( sourceType ); - var expression = sourceType == typeof( JsonElement ) - ? FilterExpressionParser.Parse( filter, new ParseExpressionContext( - param, - param, - new ElementTypeDescriptor() ) ) - : FilterExpressionParser.Parse( filter, new ParseExpressionContext( - param, - param, - new NodeTypeDescriptor() ) ); - - return (expression, param); + if ( sourceType == typeof( JsonElement ) ) + { + var elementContext = new FilterContext( new ElementTypeDescriptor() ); + return (FilterParser.Parse( filter, elementContext ), elementContext.Root); + } + + var nodeContext = new FilterContext( new NodeTypeDescriptor() ); + return (FilterParser.Parse( filter, nodeContext ), nodeContext.Root); } private static bool Execute( Expression expression, ParameterExpression param, Type sourceType ) @@ -195,7 +211,7 @@ private static bool CompileAndExecute( string filter, Type sourceType ) if ( sourceType == typeof( JsonElement ) ) { var source = GetDocument(); - var func = FilterExpressionParser.Compile( filter, new ElementTypeDescriptor() ); + var func = FilterParser.Compile( filter, new ElementTypeDescriptor() ); return func( source.RootElement, source.RootElement ); } @@ -203,7 +219,7 @@ private static bool CompileAndExecute( string filter, Type sourceType ) { // arrange var source = GetDocument(); - var func = FilterExpressionParser.Compile( filter, new NodeTypeDescriptor() ); + var func = FilterParser.Compile( filter, new NodeTypeDescriptor() ); // act return func( source, source ); diff --git a/test/Hyperbee.Json.Tests/Evaluators/FilterExtenstionFunctionTests.cs b/test/Hyperbee.Json.Tests/Evaluators/FilterExtenstionFunctionTests.cs new file mode 100644 index 00000000..9a20bb93 --- /dev/null +++ b/test/Hyperbee.Json.Tests/Evaluators/FilterExtenstionFunctionTests.cs @@ -0,0 +1,53 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using System.Text.Json; +using System.Text.Json.Nodes; +using Hyperbee.Json.Descriptors.Element; +using Hyperbee.Json.Descriptors.Node; +using Hyperbee.Json.Extensions; +using Hyperbee.Json.Filters.Parser; +using Hyperbee.Json.Tests.TestSupport; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Expression = System.Linq.Expressions.Expression; + +namespace Hyperbee.Json.Tests.Evaluators; + +[TestClass] +public class FilterExtensionFunctionTests : JsonTestBase +{ + [TestMethod] + public void Should_CallCustomFunction() + { + // arrange + var source = GetDocument(); + + JsonTypeDescriptorRegistry.GetDescriptor().Functions + .Register( PathNodeFunction.Name, () => new PathNodeFunction() ); + + // act + var results = source.Select( "$..[?path(@) == '$.store.book[2].title']" ).ToList(); + + // assert + Assert.IsTrue( results.Count == 1 ); + Assert.AreEqual( "$.store.book[2].title", results[0].GetPath() ); + } +} + +public class PathNodeFunction() : FilterExtensionFunction( argumentCount: 1 ) +{ + public const string Name = "path"; + private static readonly Expression PathExpression = Expression.Constant( (Func, string>) Path ); + + protected override Expression GetExtensionExpression( Expression[] arguments ) + { + return Expression.Invoke( PathExpression, arguments[0] ); + } + + public static string Path( IEnumerable nodes ) + { + var node = nodes.FirstOrDefault(); + return node?.GetPath(); + } +} diff --git a/test/Hyperbee.Json.Tests/Builder/JsonPathBuilderTests.cs b/test/Hyperbee.Json.Tests/Resolver/JsonPathBuilderTests.cs similarity index 90% rename from test/Hyperbee.Json.Tests/Builder/JsonPathBuilderTests.cs rename to test/Hyperbee.Json.Tests/Resolver/JsonPathBuilderTests.cs index 2f5ceee7..48c107ae 100644 --- a/test/Hyperbee.Json.Tests/Builder/JsonPathBuilderTests.cs +++ b/test/Hyperbee.Json.Tests/Resolver/JsonPathBuilderTests.cs @@ -3,7 +3,7 @@ using Hyperbee.Json.Tests.TestSupport; using Microsoft.VisualStudio.TestTools.UnitTesting; -namespace Hyperbee.Json.Tests.Builder; +namespace Hyperbee.Json.Tests.Resolver; [TestClass] public class JsonPathBuilderTests : JsonTestBase @@ -18,7 +18,7 @@ public void Should_GetPath( string key, string expected ) var source = GetDocument(); var target = source.RootElement.GetPropertyFromPath( key ); - var builder = new JsonPathBuilder( source ); + var builder = new JsonPathResolver( source ); var result = builder.GetPath( target ); Assert.AreEqual( result, expected );