Skip to content

Commit

Permalink
Feature/30 feature test with jsonpath compliance test suite (#35)
Browse files Browse the repository at this point in the history
- This project pulls the cts.json directly from the Compliance Test Suite
- Improved handling of whitespace
- Validation of non-singular queries and constants
- Added support for I-Regexp format (RFC-9485)​.
- Improved filter expression parsing
- Switched to using an internal type system for comparing values
- Memory and performance improvements

---------

Co-authored-by: Brenton Farmer <[email protected]>
  • Loading branch information
MattEdwardsWaggleBee and bfarmer67 authored Jul 12, 2024
1 parent 4d192af commit 6f56d32
Show file tree
Hide file tree
Showing 80 changed files with 25,858 additions and 1,707 deletions.
9 changes: 8 additions & 1 deletion Hyperbee.Json.sln
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,13 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".github", ".github", "{1FA7
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "workflows", "workflows", "{4DBDB7F5-3F66-4572-80B5-3322449C77A4}"
ProjectSection(SolutionItems) = preProject
.github\workflows\create-prerelease.yml = .github\workflows\create-prerelease.yml
.github\workflows\create-release.yml = .github\workflows\create-release.yml
.github\workflows\format.yml = .github\workflows\format.yml
.github\workflows\issue-branch.yml = .github\workflows\issue-branch.yml
.github\workflows\publish.yml = .github\workflows\publish.yml
.github\workflows\test-report.yml = .github\workflows\test-report.yml
.github\workflows\test.yml = .github\workflows\test.yml
.github\workflows\update-version.yml = .github\workflows\update-version.yml
EndProjectSection
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Hyperbee.Json.Tests", "test\Hyperbee.Json.Tests\Hyperbee.Json.Tests.csproj", "{97886205-1467-4EE6-B3DA-496CA3D086E4}"
Expand All @@ -41,6 +41,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "docs", "docs", "{13CB9B41-0
docs\JSONPATH-SYNTAX.md = docs\JSONPATH-SYNTAX.md
EndProjectSection
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Hyperbee.Json.Cts", "test\Hyperbee.Json.Cts\Hyperbee.Json.Cts.csproj", "{CC1D3E7F-E6F1-432B-B4D1-9402AED24119}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand All @@ -59,6 +61,10 @@ Global
{45C24D4B-4A0B-4FF1-AC66-38374D2455E9}.Debug|Any CPU.Build.0 = Debug|Any CPU
{45C24D4B-4A0B-4FF1-AC66-38374D2455E9}.Release|Any CPU.ActiveCfg = Release|Any CPU
{45C24D4B-4A0B-4FF1-AC66-38374D2455E9}.Release|Any CPU.Build.0 = Release|Any CPU
{CC1D3E7F-E6F1-432B-B4D1-9402AED24119}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{CC1D3E7F-E6F1-432B-B4D1-9402AED24119}.Debug|Any CPU.Build.0 = Debug|Any CPU
{CC1D3E7F-E6F1-432B-B4D1-9402AED24119}.Release|Any CPU.ActiveCfg = Release|Any CPU
{CC1D3E7F-E6F1-432B-B4D1-9402AED24119}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand All @@ -69,6 +75,7 @@ Global
{97886205-1467-4EE6-B3DA-496CA3D086E4} = {F9B24CD9-E06B-4834-84CB-8C29E5F10BE0}
{45C24D4B-4A0B-4FF1-AC66-38374D2455E9} = {F9B24CD9-E06B-4834-84CB-8C29E5F10BE0}
{13CB9B41-0462-4812-8B13-0BFD17F2BC18} = {870D9301-BE3D-44EA-BF9C-FCC2E87FE4CD}
{CC1D3E7F-E6F1-432B-B4D1-9402AED24119} = {F9B24CD9-E06B-4834-84CB-8C29E5F10BE0}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {32874F5B-B467-4F28-A8E2-82C2536FB228}
Expand Down
1 change: 1 addition & 0 deletions Hyperbee.Json.sln.DotSettings
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ECSharpUseContinuousIndentInsideBracesMigration/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ESettingsUpgrade_002EMigrateBlankLinesAroundFieldToBlankLinesAroundProperty/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ESettingsUpgrade_002EPredefinedNamingRulesToUserRulesUpgrade/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/GrammarAndSpelling/GrammarChecking/Exceptions/=sub_002Dstate/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/GrammarAndSpelling/GrammarChecking/Exceptions/=the_0020expire/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/GrammarAndSpelling/GrammarChecking/Exceptions/=the_0020if/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=ABNF/@EntryIndexedValue">True</s:Boolean>
Expand Down
93 changes: 45 additions & 48 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,12 @@ The library is designed to be quick and extensible, allowing support for other J
- **`IEnumerable` Results:** Deferred execution queries with `IEnumerable`.
- **Conformant:** Adheres to the JSONPath Specification [RFC 9535](https://www.rfc-editor.org/rfc/rfc9535.html).

## JSONPath Consensus
## JSONPath RFC

Hyperbee.Json aims to follow the RFC, and to support the [JSONPath consensus](https://cburgmer.github.io/json-path-comparison)
Hyperbee.Json conforms to the RFC, and aims to support the [JSONPath consensus](https://cburgmer.github.io/json-path-comparison)
when the RFC is unopinionated. When the RFC is unopinionated, and where the consensus is ambiguous or not aligned with our
performance and usability goals, we may deviate. Our goal is always to provide a robust and performant library while
strengthening our alignment with the RFC.
strengthening our alignment with the RFC and the community.

## Installation

Expand Down Expand Up @@ -193,21 +193,18 @@ You can also extend the supported function set by registering your own functions
**Step 1:** Create a custom function that returns the path of a `JsonNode`.

```csharp
public class PathNodeFunction() : FilterExtensionFunction( argumentCount: 1 )
public class PathNodeFunction() : FilterExtensionFunction( PathMethodInfo, FilterExtensionInfo.MustCompare )
{
public const string Name = "path";
private static readonly Expression PathExpression = Expression.Constant( (Func<IEnumerable<JsonNode>, string>) Path );
private static readonly MethodInfo PathMethodInfo = GetMethod<PathNodeFunction>( nameof( Path ) );

protected override Expression GetExtensionExpression( Expression[] arguments )
private static INodeType Path( INodeType arg )
{
return Expression.Invoke( PathExpression, arguments[0] );
}
if ( arg is not NodesType<JsonNode> nodes )
return Constants.Null;

public static string Path( IEnumerable<JsonNode> nodes )
{
var node = nodes.FirstOrDefault();
return node?.GetPath();
}
return new ValueType<string>( node?.GetPath() );
}
```

Expand All @@ -231,7 +228,6 @@ 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.
Expand Down Expand Up @@ -268,7 +264,7 @@ There are excellent libraries available for RFC-9535 .NET JsonPath.
- Supports both `JsonElement`, and `JsonNode`.
- Deferred execution queries with `IEnumerable`.
- Extendable to support additional JSON document types and functions.
- RFC and Consensus focused JSONPath implementation.
- RFC conforming JSONPath implementation.

## Benchmarks

Expand Down Expand Up @@ -314,52 +310,53 @@ Here is a performance comparison of various queries on the standard book store d
```

```
| Method | Filter | Mean | Error | StdDev | Allocated
|:----------------------- |:-------------------------------- |:--------- |:--------- |:--------- |:---------
| Hyperbee_JsonElement | $..* `First()` | 3.026 us | 0.3647 us | 0.0200 us | 4.22 KB
| JsonEverything_JsonNode | $..* `First()` | 3.170 us | 0.3034 us | 0.0166 us | 3.53 KB
| Hyperbee_JsonNode | $..* `First()` | 3.275 us | 1.7533 us | 0.0961 us | 3.37 KB
| JsonCons_JsonElement | $..* `First()` | 5.699 us | 0.2191 us | 0.0120 us | 8.48 KB
| Newtonsoft_JObject | $..* `First()` | 8.671 us | 1.7810 us | 0.0976 us | 14.22 KB
| | | | | |
| JsonCons_JsonElement | $..* | 5.772 us | 3.8960 us | 0.2136 us | 8.45 KB
| Hyperbee_JsonElement | $..* | 8.179 us | 4.9380 us | 0.2707 us | 11.02 KB
| Newtonsoft_JObject | $..* | 9.867 us | 0.9006 us | 0.0494 us | 14.86 KB
| Hyperbee_JsonNode | $..* | 10.188 us | 2.0528 us | 0.1125 us | 10.83 KB
| JsonEverything_JsonNode | $..* | 21.124 us | 5.1117 us | 0.2802 us | 36.81 KB
| | | | | |
| Hyperbee_JsonElement | $..price | 4.867 us | 0.1883 us | 0.0103 us | 6.37 KB
| JsonCons_JsonElement | $..price | 4.924 us | 1.5997 us | 0.0877 us | 5.65 KB
| Hyperbee_JsonNode | $..price | 7.827 us | 5.0475 us | 0.2767 us | 8.77 KB
| Newtonsoft_JObject | $..price | 9.442 us | 1.0020 us | 0.0549 us | 14.4 KB
| JsonEverything_JsonNode | $..price | 15.865 us | 2.1515 us | 0.1179 us | 27.63 KB
| | | | | |
| Hyperbee_JsonElement | $.store.book[?(@.price == 8.99)] | 4.550 us | 1.0340 us | 0.0567 us | 9.08 KB
| JsonCons_JsonElement | $.store.book[?(@.price == 8.99)] | 5.341 us | 1.0738 us | 0.0589 us | 5.05 KB
| Hyperbee_JsonNode | $.store.book[?(@.price == 8.99)] | 7.341 us | 3.6147 us | 0.1981 us | 10.63 KB
| Newtonsoft_JObject | $.store.book[?(@.price == 8.99)] | 9.621 us | 5.1553 us | 0.2826 us | 15.84 KB
| JsonEverything_JsonNode | $.store.book[?(@.price == 8.99)] | 11.789 us | 5.2457 us | 0.2875 us | 15.85 KB
| | | | | |
| Hyperbee_JsonElement | $.store.book[0] | 2.896 us | 0.1069 us | 0.0059 us | 3.41 KB
| JsonCons_JsonElement | $.store.book[0] | 2.967 us | 0.1084 us | 0.0059 us | 3.21 KB
| Hyperbee_JsonNode | $.store.book[0] | 3.352 us | 0.1778 us | 0.0097 us | 3.58 KB
| JsonEverything_JsonNode | $.store.book[0] | 4.779 us | 2.9031 us | 0.1591 us | 5.96 KB
| Newtonsoft_JObject | $.store.book[0] | 8.714 us | 2.5518 us | 0.1399 us | 14.56 KB
```
| Method | Filter | Mean | Error | StdDev | Allocated
|------------------------ |--------------------------------- |---------- |----------- |---------- |----------
| Hyperbee_JsonElement | $..* `First()` | 3.186 us | 0.6615 us | 0.0363 us | 4.3 KB
| Hyperbee_JsonNode | $..* `First()` | 3.521 us | 0.1192 us | 0.0065 us | 3.45 KB
| JsonEverything_JsonNode | $..* `First()` | 3.545 us | 0.7400 us | 0.0406 us | 3.53 KB
| JsonCons_JsonElement | $..* `First()` | 5.793 us | 1.3811 us | 0.0757 us | 8.48 KB
| Newtonsoft_JObject | $..* `First()` | 9.119 us | 5.3278 us | 0.2920 us | 14.22 KB
| | | | | |
| JsonCons_JsonElement | $..* | 6.098 us | 2.0947 us | 0.1148 us | 8.45 KB
| Hyperbee_JsonElement | $..* | 8.812 us | 1.6812 us | 0.0922 us | 11.1 KB
| Hyperbee_JsonNode | $..* | 10.621 us | 1.2452 us | 0.0683 us | 10.91 KB
| Newtonsoft_JObject | $..* | 11.037 us | 5.4690 us | 0.2998 us | 14.86 KB
| JsonEverything_JsonNode | $..* | 23.329 us | 2.2255 us | 0.1220 us | 36.81 KB
| | | | | |
| Hyperbee_JsonElement | $..price | 5.248 us | 3.4306 us | 0.1880 us | 6.45 KB
| JsonCons_JsonElement | $..price | 5.402 us | 0.3285 us | 0.0180 us | 5.65 KB
| Hyperbee_JsonNode | $..price | 8.483 us | 2.0999 us | 0.1151 us | 8.86 KB
| Newtonsoft_JObject | $..price | 10.109 us | 9.6403 us | 0.5284 us | 14.4 KB
| JsonEverything_JsonNode | $..price | 17.054 us | 10.5303 us | 0.5772 us | 27.63 KB
| | | | | |
| Hyperbee_JsonElement | $.store.book[?(@.price == 8.99)] | 4.486 us | 3.2931 us | 0.1805 us | 5.82 KB
| JsonCons_JsonElement | $.store.book[?(@.price == 8.99)] | 5.381 us | 3.3826 us | 0.1854 us | 5.05 KB
| Hyperbee_JsonNode | $.store.book[?(@.price == 8.99)] | 7.354 us | 4.9887 us | 0.2734 us | 8.47 KB
| Newtonsoft_JObject | $.store.book[?(@.price == 8.99)] | 10.519 us | 3.5514 us | 0.1947 us | 15.84 KB
| JsonEverything_JsonNode | $.store.book[?(@.price == 8.99)] | 11.912 us | 7.6346 us | 0.4185 us | 15.85 KB
| | | | | |
| Hyperbee_JsonElement | $.store.book[0] | 2.722 us | 0.5813 us | 0.0319 us | 2.27 KB
| JsonCons_JsonElement | $.store.book[0] | 3.150 us | 1.7316 us | 0.0949 us | 3.21 KB
| Hyperbee_JsonNode | $.store.book[0] | 3.339 us | 0.1733 us | 0.0095 us | 2.77 KB
| JsonEverything_JsonNode | $.store.book[0] | 4.974 us | 3.2013 us | 0.1755 us | 5.96 KB
| Newtonsoft_JObject | $.store.book[0] | 9.482 us | 7.0303 us | 0.3854 us | 14.56 KB

```

## Additional Documentation
## Additioal Documentation

Additional documentation can be found in the project's `/docs` folder.

## Credits

Hyperbee.Json is built upon the great work of several open-source projects. Special thanks to:

- Stefan Goessner for the original [JSONPath implementation](https://goessner.net/articles/JsonPath/).
- System.Text.Json team for their work on the `System.Text.Json` library.
- Stefan Goessner for the original [JSONPath implementation](https://goessner.net/articles/JsonPath/).
- Atif Aziz's C# port of Goessner's JSONPath library [.NET JSONPath](https://github.com/atifaziz/JSONPath).
- Christoph Burgmer [JSONPath consensus effort](https://cburgmer.github.io/json-path-comparison).
- [JSONPath Compliance Test Suite Team](https://github.com/jsonpath-standard/jsonpath-compliance-test-suite).

## Contributing

Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
using System.Text.Json;
using Hyperbee.Json.Descriptors.Element.Functions;
using Hyperbee.Json.Filters;
using Hyperbee.Json.Filters.Values;

namespace Hyperbee.Json.Descriptors.Element;

public class ElementTypeDescriptor : ITypeDescriptor<JsonElement>
{
private FilterEvaluator<JsonElement> _evaluator;
private ElementValueAccessor _accessor;
private NodeTypeComparer<JsonElement> _comparer;

public FunctionRegistry Functions { get; } = new();

Expand All @@ -17,6 +19,11 @@ public class ElementTypeDescriptor : ITypeDescriptor<JsonElement>
public IFilterEvaluator<JsonElement> FilterEvaluator =>
_evaluator ??= new FilterEvaluator<JsonElement>( this );

public INodeTypeComparer Comparer =>
_comparer ??= new NodeTypeComparer<JsonElement>( Accessor );

public bool CanUsePointer => true;

public ElementTypeDescriptor()
{
Functions.Register( CountElementFunction.Name, () => new CountElementFunction() );
Expand Down
30 changes: 26 additions & 4 deletions src/Hyperbee.Json/Descriptors/Element/ElementValueAccessor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,18 @@ internal class ElementValueAccessor : IValueAccessor<JsonElement>
}

[MethodImpl( MethodImplOptions.AggressiveInlining )]
public JsonElement GetElementAt( in JsonElement value, int index )
public bool TryGetElementAt( in JsonElement value, int index, out JsonElement element )
{
return value[index];
element = default;

if ( index < 0 ) // flip negative index to positive
index = value.GetArrayLength() + index;

if ( index < 0 || index >= value.GetArrayLength() ) // out of bounds
return false;

element = value[index];
return true;
}

[MethodImpl( MethodImplOptions.AggressiveInlining )]
Expand All @@ -67,7 +76,7 @@ public int GetArrayLength( in JsonElement value )
: 0;
}

public bool TryGetChildValue( in JsonElement value, string childSelector, out JsonElement childValue )
public bool TryGetChildValue( in JsonElement value, string childSelector, SelectorKind selectorKind, out JsonElement childValue )
{
switch ( value.ValueKind )
{
Expand All @@ -77,9 +86,17 @@ public bool TryGetChildValue( in JsonElement value, string childSelector, out Js
break;

case JsonValueKind.Array:
if ( selectorKind == SelectorKind.Name )
break;

if ( int.TryParse( childSelector, NumberStyles.Integer, CultureInfo.InvariantCulture, out var index ) )
{
if ( index >= 0 && index < value.GetArrayLength() )
var arrayLength = value.GetArrayLength();

if ( index < 0 ) // flip negative index to positive
index = arrayLength + index;

if ( index >= 0 && index < arrayLength )
{
childValue = value[index];
return true;
Expand Down Expand Up @@ -163,4 +180,9 @@ public bool TryGetValueFromNode( JsonElement element, out object value )

return true;
}

public bool TryGetFromPointer( in JsonElement element, JsonPathSegment segment, out JsonElement childValue )
{
return element.TryGetFromJsonPathPointer( segment, out childValue );
}
}
Original file line number Diff line number Diff line change
@@ -1,22 +1,25 @@
using System.Linq.Expressions;
using System.Reflection;
using System.Text.Json;

using Hyperbee.Json.Filters.Parser;
using Hyperbee.Json.Filters.Values;

namespace Hyperbee.Json.Descriptors.Element.Functions;

public class CountElementFunction() : FilterExtensionFunction( argumentCount: 1 )
public class CountElementFunction() : FilterExtensionFunction( CountMethodInfo, FilterExtensionInfo.MustCompare )
{
public const string Name = "count";
private static readonly Expression CountExpression = Expression.Constant( (Func<IEnumerable<JsonElement>, float>) Count );

protected override Expression GetExtensionExpression( Expression[] arguments )
{
return Expression.Invoke( CountExpression, arguments[0] );
}
private static readonly MethodInfo CountMethodInfo = GetMethod<CountElementFunction>( nameof( Count ) );

public static float Count( IEnumerable<JsonElement> elements )
public static INodeType Count( INodeType input )
{
return elements.Count();
switch ( input )
{
case NodesType<JsonElement> nodes:
if ( nodes.IsNormalized && !nodes.Any() )
return new ValueType<float>( 1F );
return new ValueType<float>( nodes.Count() );
default:
return new ValueType<float>( 1F );
}
}
}
Loading

0 comments on commit 6f56d32

Please sign in to comment.