Skip to content

Commit

Permalink
Release v1.3.0 (#36)
Browse files Browse the repository at this point in the history
* [FEATURE]: Improve parser error handling (#32)

* Improve query parser error handling of quoted and unquoted select names
* Correctly throw when there are unexpected characters at the end of a query
* Refactored JsonHelper and extensions

---------

Co-authored-by: Brenton Farmer <[email protected]>

* [FEATURE]: Improve documentation and comments (#34)

* Update comments and documentation

---------

Co-authored-by: Brenton Farmer <[email protected]>

* Feature/30 feature test with jsonpath compliance test suite (#35)

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

* Previous version was 'v1.2.1'. Version now 'v1.3.0'.

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Brenton Farmer <[email protected]>
Co-authored-by: MattEdwardsWaggleBee <[email protected]>
  • Loading branch information
4 people authored Jul 12, 2024
1 parent 85f895e commit 55a9273
Show file tree
Hide file tree
Showing 89 changed files with 26,075 additions and 1,878 deletions.
4 changes: 2 additions & 2 deletions Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@
<!-- Solution version numbers -->
<PropertyGroup>
<MajorVersion>1</MajorVersion>
<MinorVersion>2</MinorVersion>
<PatchVersion>1</PatchVersion>
<MinorVersion>3</MinorVersion>
<PatchVersion>0</PatchVersion>
</PropertyGroup>
<!-- Disable automatic package publishing -->
<PropertyGroup>
Expand Down
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
174 changes: 71 additions & 103 deletions README.md

Large diffs are not rendered by default.

43 changes: 24 additions & 19 deletions docs/ADDITIONAL-CLASSES.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,28 +5,18 @@ property diving, element comparisons, and dynamic property access.

### Property Diving

Property diving acts similarly to JSON Pointer; it expects a path that returns a single element.
Unlike JSON Pointer, property diving notation expects a singular JSON Path.
Property diving acts **similarly** to JSON Pointer; it expects an absolute path that returns a single element.
Unlike JSON Pointer, property diving notation expects normalized JSON Path notation.

| Method | Description
|:-----------------------------------|:-----------
| `JsonElement.FromJsonPathPointer` | Dives for properties using absolute locations like `$.store.book[2].author`

The syntax supports singular paths; dotted notation, quoted names, and simple bracketed array accessors only.
The syntax supports absolute (normalized) paths; dotted notation, quoted names, and simple bracketed array accessors only.
The intention is to return a single element by literal path.

Json path style '$', wildcard '*', '..', and '[a,b]' multi-result selector notations and filters are **not** supported.
Json path style wildcard '*', '..', and '[a,b]' multi-result selector notations and filters are **not** supported.

```
Examples of valid path syntax:
prop1.prop2
prop1[0]
'prop.2'
prop1[0].prop2
prop1['prop.2']
prop1.'prop.2'[0].prop3
```

### JsonElement Path

Expand All @@ -35,7 +25,7 @@ for a given `JsonElement`.

| Method | Description
|:---------------------------|:-----------
| `JsonPathBuilder.GetPath` | Returns the JsonPath location string for a given element
| `JsonPathBuilder.GetPath` | Returns the JsonPath location string for a given element

### Equality Helpers

Expand All @@ -47,18 +37,33 @@ for a given `JsonElement`.
### Dynamic Object Serialization

Basic support is provided for serializing to and from dynamic objects through the use of a custom `JsonConverter`.
The `DynamicJsonConverter` converter class is useful for simple scenareos. It is intended as a simple helper for
basic use cases only.
The `DynamicJsonConverter` class is useful for simple scenareos. It is intended as a simple helper for
basic use cases only. A helper methods `JsonHelper.ConvertToDynamic` is provided to simplify the process of
serializing and deserializing dynamic objects.

#### Example: ConvertToDynamic

```csharp
var root = JsonDocument.Parse(jsonInput); // jsonInput contains the bookstore example
var element = JsonHelper.ConvertToDynamic( source );

var book = element.store.book[0];
var author = book.author;
var price = book.price;

Assert.IsTrue( price == 8.95 );
Assert.IsTrue( author == "Nigel Rees" );
```

#### DynamicJsonConverter
#### Example: Serialize To Dynamic

```csharp
var serializerOptions = new JsonSerializerOptions
{
Converters = {new DynamicJsonConverter()}
};

// jsonInput is a string containing the bookstore json from the previous examples
// jsonInput contains the bookstore example
var jobject = JsonSerializer.Deserialize<dynamic>( jsonInput, serializerOptions);

Assert.IsTrue( jobject.store.bicycle.color == "red" );
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 );
}
}
}
Original file line number Diff line number Diff line change
@@ -1,28 +1,42 @@
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 LengthElementFunction() : FilterExtensionFunction( argumentCount: 1 )
public class LengthElementFunction() : FilterExtensionFunction( LengthMethodInfo, FilterExtensionInfo.MustCompare | FilterExtensionInfo.ExpectNormalized )
{
public const string Name = "length";
private static readonly Expression LengthExpression = Expression.Constant( (Func<IEnumerable<JsonElement>, float>) Length );
private static readonly MethodInfo LengthMethodInfo = GetMethod<LengthElementFunction>( nameof( Length ) );

protected override Expression GetExtensionExpression( Expression[] arguments )
public static INodeType Length( INodeType input )
{
return Expression.Invoke( LengthExpression, arguments[0] );
return input switch
{
NodesType<JsonElement> nodes => LengthImpl( nodes.FirstOrDefault() ),
ValueType<string> valueString => new ValueType<float>( valueString.Value.Length ),
Null or Nothing => input,
_ => Constants.Nothing
};
}

public static float Length( IEnumerable<JsonElement> elements )
public static INodeType LengthImpl( object value )
{
var element = elements.FirstOrDefault();
return element.ValueKind switch
return value switch
{
JsonValueKind.String => element.GetString()?.Length ?? 0,
JsonValueKind.Array => element.GetArrayLength(),
JsonValueKind.Object => element.EnumerateObject().Count(),
_ => 0
string str => new ValueType<float>( str.Length ),
Array array => new ValueType<float>( array.Length ),
System.Collections.ICollection collection => new ValueType<float>( collection.Count ),
System.Collections.IEnumerable enumerable => new ValueType<float>( enumerable.Cast<object>().Count() ),
JsonElement node => node.ValueKind switch
{
JsonValueKind.String => new ValueType<float>( node.GetString()?.Length ?? 0 ),
JsonValueKind.Array => new ValueType<float>( node.EnumerateArray().Count() ),
JsonValueKind.Object => new ValueType<float>( node.EnumerateObject().Count() ),
_ => Constants.Null
},
_ => Constants.Null
};
}
}
Original file line number Diff line number Diff line change
@@ -1,30 +1,39 @@
using System.Linq.Expressions;
using System.Reflection;
using System.Text.Json;
using System.Text.RegularExpressions;
using Hyperbee.Json.Filters;
using Hyperbee.Json.Filters.Parser;
using Hyperbee.Json.Filters.Values;

namespace Hyperbee.Json.Descriptors.Element.Functions;

public class MatchElementFunction() : FilterExtensionFunction( argumentCount: 2 )
public class MatchElementFunction() : FilterExtensionFunction( MatchMethodInfo, FilterExtensionInfo.MustNotCompare )
{
public const string Name = "match";
private static readonly Expression MatchExpression = Expression.Constant( (Func<IEnumerable<JsonElement>, string, bool>) Match );
private static readonly MethodInfo MatchMethodInfo = GetMethod<MatchElementFunction>( nameof( Match ) );

protected override Expression GetExtensionExpression( Expression[] arguments )
public static INodeType Match( INodeType input, INodeType regex )
{
return Expression.Invoke( MatchExpression, arguments[0], arguments[1] );
return input switch
{
NodesType<JsonElement> nodes when regex is ValueType<string> stringValue =>
MatchImpl( nodes, stringValue.Value ),
NodesType<JsonElement> nodes when regex is NodesType<JsonElement> stringValue =>
MatchImpl( nodes, stringValue.Value.FirstOrDefault().GetString() ),
_ => Constants.False
};
}

public static bool Match( IEnumerable<JsonElement> elements, string regex )
public static INodeType MatchImpl( NodesType<JsonElement> nodes, string regex )
{
var value = elements.FirstOrDefault().GetString();
var value = nodes.FirstOrDefault();

if ( value == null )
{
return false;
}
if ( value.ValueKind != JsonValueKind.String )
return Constants.False;

var stringValue = value.GetString() ?? string.Empty;

var regexPattern = new Regex( regex.Trim( '\"', '\'' ) );
return regexPattern.IsMatch( $"^{value}$" );
var regexPattern = new Regex( $"^{IRegexp.ConvertToIRegexp( regex )}$" );
return new ValueType<bool>( regexPattern.IsMatch( stringValue ) );
}
}
Loading

0 comments on commit 55a9273

Please sign in to comment.