Skip to content

Commit

Permalink
Merge branch 'develop'
Browse files Browse the repository at this point in the history
  • Loading branch information
bfarmer67 committed Jul 22, 2024
2 parents 55a9273 + 02d9f89 commit 15450f8
Show file tree
Hide file tree
Showing 118 changed files with 11,284 additions and 9,252 deletions.
2 changes: 1 addition & 1 deletion Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
<!-- Solution version numbers -->
<PropertyGroup>
<MajorVersion>1</MajorVersion>
<MinorVersion>3</MinorVersion>
<MinorVersion>4</MinorVersion>
<PatchVersion>0</PatchVersion>
</PropertyGroup>
<!-- Disable automatic package publishing -->
Expand Down
117 changes: 63 additions & 54 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,7 @@ or bracket-notation:
- JSONPath allows the wildcard symbol `*` for member names and array indices.
- It borrows the descendant operator `..` from [E4X][e4x]
- It uses the `@` symbol to refer to the current object.
- It uses the `?()` syntax for filtering.
- It uses `?` syntax for filtering.
- It uses the array slice syntax proposal `[start:end:step]` from ECMASCRIPT 4.

Expressions can be used as an alternative to explicit names or indices, as in:
Expand All @@ -173,38 +173,46 @@ Filter expressions are supported via the syntax `?(<boolean expr>)`, as in:

### JSONPath Functions

JsonPath expressions support basic methods calls.
JsonPath expressions support basic method calls.

| Method | Description | Example
|------------|--------------------------------------------------------|------------------------------------------------
| `length()` | Returns the length of an array or string. | `$.store.book[?(length(@.title) > 5)]`
| `count()` | Returns the count of matching elements. | `$.store.book[?(count(@.authors.) > 1)]`
| `count()` | Returns the count of matching elements. | `$.store.book[?(count(@.authors) > 1)]`
| `match()` | Returns true if a string matches a regular expression. | `$.store.book[?(match(@.title,'.*Century.*'))]`
| `search()` | Searches for a string within another string. | `$.store.book[?(search(@.title,'Sword'))]`
| `value()` | Accesses the value of a key in the current object. | `$.store.book[?(value(@.price) < 10)]`

### JSONPath Extended Syntax

The library extends the JSONPath expression syntax to support additional features.

| Operators | Description | Example
|---------------------|-----------------------------------------------|------------------------------------------------
| `+` `-` `*` `\` `%` | Basic math operators. | `$[?(@.a + @.b == 3)]`
| `in` | Tests is a value is in a set. | `$[[email protected] in ['a', 'b', 'c'] ]`


### JSONPath Custom Functions

You can also extend the supported function set by registering your own functions.
You can extend the supported function set by registering your own functions.

**Example:** Implement a `JsonNode` Path Function:

**Example:** Implement a `JsonNode` Path Function:

**Step 1:** Create a custom function that returns the path of a `JsonNode`.

```csharp
public class PathNodeFunction() : FilterExtensionFunction( PathMethodInfo, FilterExtensionInfo.MustCompare )
public class PathNodeFunction() : ExtensionFunction( PathMethod, CompareConstraint.MustCompare )
{
public const string Name = "path";
private static readonly MethodInfo PathMethodInfo = GetMethod<PathNodeFunction>( nameof( Path ) );
private static readonly MethodInfo PathMethod = GetMethod<PathNodeFunction>( nameof( Path ) );

private static INodeType Path( INodeType arg )
private static ScalarValue<string> Path( IValueType argument )
{
if ( arg is not NodesType<JsonNode> nodes )
return Constants.Null;

var node = nodes.FirstOrDefault();
return new ValueType<string>( node?.GetPath() );
return argument.TryGetNode<JsonNode>( out var node ) ? node?.GetPath() : null;
}
}
```

Expand All @@ -221,6 +229,15 @@ JsonTypeDescriptorRegistry.GetDescriptor<JsonNode>().Functions
var results = source.Select( "$..[?path(@) == '$.store.book[2].title']" );
```

## Why Choose [Hyperbee.Json](https://github.com/Stillpoint-Software/Hyperbee.Json) ?

- High Performance.
- Supports both `JsonElement`, and `JsonNode`.
- Deferred execution queries with `IEnumerable`.
- Enhanced JsonPath syntax.
- Extendable to support additional JSON document types.
- RFC conforming JSONPath implementation.

## Comparison with Other Libraries

There are excellent libraries available for RFC-9535 .NET JsonPath.
Expand All @@ -230,11 +247,12 @@ There are excellent libraries available for RFC-9535 .NET JsonPath.
- **Pros:**
- Comprehensive feature set.
- Deferred execution queries with `IEnumerable`.
- Enhanced JsonPath syntax.
- Strong community support.
- .NET Foundation Project.

- **Cons:**
- No support for `JsonElement`.
- More memory intensive.
- Not quite as fast as other `System.Text.Json` implementations.

### [JsonCons.NET](https://danielaparker.github.io/JsonCons.Net/articles/JsonPath/JsonConsJsonPath.html)
Expand All @@ -251,21 +269,13 @@ There are excellent libraries available for RFC-9535 .NET JsonPath.

- **Pros:**
- Comprehensive feature set.
- Deferred execution queries with `IEnumerable`.
- Documentation and examples.
- Strong community support.
- .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.
- RFC conforming JSONPath implementation.

## Benchmarks

Here is a performance comparison of various queries on the standard book store document.
Expand Down Expand Up @@ -310,38 +320,37 @@ Here is a performance comparison of various queries on the standard book store d
```

```
| 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

Method | Filter | Mean | Error | StdDev | Allocated
--------------------------------- |--------------------------------- |-----------|------------|-----------|----------
JsonPath_Hyperbee_JsonElement | $..* `First()` | 3.105 us | 1.6501 us | 0.0904 us | 3.52 KB
JsonPath_JsonEverything_JsonNode | $..* `First()` | 3.278 us | 3.3157 us | 0.1817 us | 3.53 KB
JsonPath_Hyperbee_JsonNode | $..* `First()` | 3.302 us | 3.2094 us | 0.1759 us | 3.09 KB
JsonPath_JsonCons_JsonElement | $..* `First()` | 6.170 us | 4.1597 us | 0.2280 us | 8.48 KB
JsonPath_Newtonsoft_JObject | $..* `First()` | 8.708 us | 8.7586 us | 0.4801 us | 14.22 KB
| | | | |
JsonPath_JsonCons_JsonElement | $..* | 5.792 us | 6.6920 us | 0.3668 us | 8.45 KB
JsonPath_Hyperbee_JsonElement | $..* | 7.504 us | 7.6479 us | 0.4192 us | 9.13 KB
JsonPath_Hyperbee_JsonNode | $..* | 10.320 us | 5.6676 us | 0.3107 us | 10.91 KB
JsonPath_Newtonsoft_JObject | $..* | 10.862 us | 0.4374 us | 0.0240 us | 14.86 KB
JsonPath_JsonEverything_JsonNode | $..* | 21.914 us | 19.4680 us | 1.0671 us | 36.81 KB
| | | | |
JsonPath_Hyperbee_JsonElement | $..price | 4.557 us | 3.6801 us | 0.2017 us | 4.2 KB
JsonPath_JsonCons_JsonElement | $..price | 4.989 us | 2.3125 us | 0.1268 us | 5.65 KB
JsonPath_Hyperbee_JsonNode | $..price | 7.929 us | 0.6128 us | 0.0336 us | 7.48 KB
JsonPath_Newtonsoft_JObject | $..price | 10.511 us | 11.4901 us | 0.6298 us | 14.4 KB
JsonPath_JsonEverything_JsonNode | $..price | 15.999 us | 0.5210 us | 0.0286 us | 27.63 KB
| | | | |
JsonPath_Hyperbee_JsonElement | $.store.book[?(@.price == 8.99)] | 4.221 us | 2.4758 us | 0.1357 us | 5.24 KB
JsonPath_JsonCons_JsonElement | $.store.book[?(@.price == 8.99)] | 5.424 us | 0.3551 us | 0.0195 us | 5.05 KB
JsonPath_Hyperbee_JsonNode | $.store.book[?(@.price == 8.99)] | 7.023 us | 7.0447 us | 0.3861 us | 8 KB
JsonPath_Newtonsoft_JObject | $.store.book[?(@.price == 8.99)] | 10.572 us | 2.4203 us | 0.1327 us | 15.84 KB
JsonPath_JsonEverything_JsonNode | $.store.book[?(@.price == 8.99)] | 12.478 us | 0.5762 us | 0.0316 us | 15.85 KB
| | | | |
JsonPath_Hyperbee_JsonElement | $.store.book[0] | 2.720 us | 1.9771 us | 0.1084 us | 2.27 KB
JsonPath_JsonCons_JsonElement | $.store.book[0] | 3.266 us | 0.2087 us | 0.0114 us | 3.21 KB
JsonPath_Hyperbee_JsonNode | $.store.book[0] | 3.396 us | 0.5137 us | 0.0282 us | 2.77 KB
JsonPath_JsonEverything_JsonNode | $.store.book[0] | 5.088 us | 0.1202 us | 0.0066 us | 5.96 KB
JsonPath_Newtonsoft_JObject | $.store.book[0] | 9.178 us | 9.5618 us | 0.5241 us | 14.56 KB
```

## Additioal Documentation
Expand Down
2 changes: 1 addition & 1 deletion docs/JSONPATH-SYNTAX.md
Original file line number Diff line number Diff line change
Expand Up @@ -372,7 +372,7 @@ Filters can be combined using logical operators `&&` (and) and `||` (or).
// Output: { "price": 15 }
```

## More Code Examples
## More Examples

### JSON Sample Document 1
```json
Expand Down
14 changes: 7 additions & 7 deletions src/Hyperbee.Json/Descriptors/Element/ElementTypeDescriptor.cs
Original file line number Diff line number Diff line change
@@ -1,26 +1,26 @@
using System.Text.Json;
using Hyperbee.Json.Descriptors.Element.Functions;
using Hyperbee.Json.Filters;
using Hyperbee.Json.Filters.Values;
using Hyperbee.Json.Filters.Parser;

namespace Hyperbee.Json.Descriptors.Element;

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

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

public IValueAccessor<JsonElement> Accessor =>
_accessor ??= new ElementValueAccessor();

public IFilterEvaluator<JsonElement> FilterEvaluator =>
_evaluator ??= new FilterEvaluator<JsonElement>( this );
public IFilterRuntime<JsonElement> FilterRuntime =>
_runtime ??= new FilterRuntime<JsonElement>();

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

public bool CanUsePointer => true;

Expand Down
71 changes: 46 additions & 25 deletions src/Hyperbee.Json/Descriptors/Element/ElementValueAccessor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,36 +10,45 @@ internal class ElementValueAccessor : IValueAccessor<JsonElement>
{
public IEnumerable<(JsonElement, string, SelectorKind)> EnumerateChildren( JsonElement value, bool includeValues = true )
{
// allocating is faster than using yield return and less memory intensive
// because we avoid calling reverse on the enumerable (which anyway allocates a new array)

switch ( value.ValueKind )
{
case JsonValueKind.Array:
{
for ( var index = value.GetArrayLength() - 1; index >= 0; index-- )
var length = value.GetArrayLength();
var results = new (JsonElement, string, SelectorKind)[length];

var reverseIndex = length - 1;
for ( var index = 0; index < length; index++, reverseIndex-- )
{
var child = value[index];

if ( includeValues || child.ValueKind is JsonValueKind.Array or JsonValueKind.Object )
yield return (child, index.ToString(), SelectorKind.Index);
{
results[reverseIndex] = (child, index.ToString(), SelectorKind.Index);
}
}

break;
return results;
}
case JsonValueKind.Object:
{
if ( includeValues )
{
foreach ( var child in value.EnumerateObject().Reverse() )
yield return (child.Value, child.Name, SelectorKind.Name);
}
else
var results = new Stack<(JsonElement, string, SelectorKind)>(); // stack will reverse the list
foreach ( var child in value.EnumerateObject() )
{
foreach ( var child in value.EnumerateObject().Where( property => property.Value.ValueKind is JsonValueKind.Array or JsonValueKind.Object ).Reverse() )
yield return (child.Value, child.Name, SelectorKind.Name);
if ( includeValues || child.Value.ValueKind is JsonValueKind.Array or JsonValueKind.Object )
{
results.Push( (child.Value, child.Name, SelectorKind.Name) );
}
}

break;
return results;
}
}

return [];
}

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

public bool TryGetChildValue( in JsonElement value, string childSelector, SelectorKind selectorKind, out JsonElement childValue )
public bool TryGetChild( in JsonElement value, string childSelector, SelectorKind selectorKind, out JsonElement childValue )
{
switch ( value.ValueKind )
{
Expand Down Expand Up @@ -127,16 +136,20 @@ static bool IsPathOperator( ReadOnlySpan<char> x )
}
}

public bool TryGetFromPointer( in JsonElement element, JsonPathSegment segment, out JsonElement childValue )
{
return element.TryGetFromJsonPathPointer( segment, out childValue );
}

// Filter Methods

public bool DeepEquals( JsonElement left, JsonElement right )
{
return left.DeepEquals( right );
}

public bool TryParseNode( ReadOnlySpan<char> item, out JsonElement element )
public bool TryParseNode( ref Utf8JsonReader reader, out JsonElement element )
{
var bytes = Encoding.UTF8.GetBytes( item.ToArray() );
var reader = new Utf8JsonReader( bytes );

try
{
if ( JsonDocument.TryParseValue( ref reader, out var document ) )
Expand All @@ -154,16 +167,29 @@ public bool TryParseNode( ReadOnlySpan<char> item, out JsonElement element )
return false;
}

public bool TryGetValueFromNode( JsonElement element, out object value )
public bool TryGetValueFromNode( JsonElement element, out IConvertible value )
{
switch ( element.ValueKind )
{
case JsonValueKind.String:
value = element.GetString();
break;
case JsonValueKind.Number:
value = element.GetSingle();
break;
if ( element.TryGetInt32( out int intValue ) )
{
value = intValue;
break;
}

if ( element.TryGetSingle( out float floatValue ) )
{
value = floatValue;
break;
}

value = false;
return false;

case JsonValueKind.True:
value = true;
break;
Expand All @@ -180,9 +206,4 @@ 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 );
}
}
Loading

0 comments on commit 15450f8

Please sign in to comment.