Skip to content

Commit

Permalink
[FEATURE]: Function Registration (#20)
Browse files Browse the repository at this point in the history
* 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 <[email protected]>
Co-authored-by: Matt Edwards <[email protected]>
  • Loading branch information
3 people authored Jun 25, 2024
1 parent bdd85df commit a25ca64
Show file tree
Hide file tree
Showing 50 changed files with 994 additions and 836 deletions.
150 changes: 93 additions & 57 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -80,8 +80,6 @@ foreach (var item in result)
}
```

### Advanced Examples

#### Filtering

```csharp
Expand Down Expand Up @@ -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
|:---------------------------------------------|:-----------------------------------------------------------
Expand Down Expand Up @@ -179,11 +177,9 @@ the syntax `?(<boolean expr>)`, 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
|------------|--------------------------------------------------------|------------------------------------------------
Expand All @@ -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<IEnumerable<JsonNode>, string>) Path );

protected override Expression GetExtensionExpression( Expression[] arguments )
{
return Expression.Invoke( PathExpression, arguments[0] );
}

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

**Step 2:** Register your custom function.

```csharp
JsonTypeDescriptorRegistry.GetDescriptor<JsonNode>().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.

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand Down
10 changes: 5 additions & 5 deletions docs/ADDITIONAL-CLASSES.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
44 changes: 11 additions & 33 deletions src/Hyperbee.Json/Descriptors/Element/ElementTypeDescriptor.cs
Original file line number Diff line number Diff line change
@@ -1,50 +1,28 @@
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;

public class ElementTypeDescriptor : ITypeDescriptor<JsonElement>
{
private FilterEvaluator<JsonElement> _evaluator;
private ElementValueAccessor _accessor;
public Dictionary<string, FunctionCreator> Functions { get; init; }

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

public IFilterEvaluator<JsonElement> FilterEvaluator
{
get => _evaluator ??= new FilterEvaluator<JsonElement>( 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<JsonElement> )
? Expression.Invoke( ValueElementFunction.ValueExpression, expression )
: expression;
}
public IValueAccessor<JsonElement> Accessor =>
_accessor ??= new ElementValueAccessor();

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

public ElementTypeDescriptor()
{
Functions = new Dictionary<string, FunctionCreator>(
[
new KeyValuePair<string, FunctionCreator>( CountElementFunction.Name, context => new CountElementFunction( context ) ),
new KeyValuePair<string, FunctionCreator>( LengthElementFunction.Name, context => new LengthElementFunction( context ) ),
new KeyValuePair<string, FunctionCreator>( MatchElementFunction.Name, context => new MatchElementFunction( context ) ),
new KeyValuePair<string, FunctionCreator>( SearchElementFunction.Name, context => new SearchElementFunction( context ) ),
new KeyValuePair<string, FunctionCreator>( 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() );
}
}
6 changes: 6 additions & 0 deletions src/Hyperbee.Json/Descriptors/Element/ElementValueAccessor.cs
Original file line number Diff line number Diff line change
@@ -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;

Expand Down Expand Up @@ -113,4 +114,9 @@ static bool IsPathOperator( ReadOnlySpan<char> x )
};
}
}

public object GetAsValue( IEnumerable<JsonElement> elements )
{
return ValueElementFunction.Value( elements );
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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<IEnumerable<JsonElement>, float>) Count );

public override Expression GetExtensionExpression( Expression[] arguments, ParseExpressionContext context )
protected override Expression GetExtensionExpression( Expression[] arguments )
{
return Expression.Invoke( CountExpression, arguments[0] );
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<IEnumerable<JsonElement>, float>) Length );

public override Expression GetExtensionExpression( Expression[] arguments, ParseExpressionContext context )
protected override Expression GetExtensionExpression( Expression[] arguments )
{
return Expression.Invoke( LengthExpression, arguments[0] );
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<IEnumerable<JsonElement>, 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<JsonElement> 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}$" );
}
}
Loading

0 comments on commit a25ca64

Please sign in to comment.