diff --git a/.gitignore b/.gitignore
index 8dd4607a..c2ddc3d9 100644
--- a/.gitignore
+++ b/.gitignore
@@ -395,4 +395,10 @@ FodyWeavers.xsd
*.msp
# JetBrains Rider
-*.sln.iml
\ No newline at end of file
+*.sln.iml
+
+# Claude AI assistant files
+.claude/
+claude.md
+**/claude-tasks.md
+**/baseline-benchmarks.md
\ No newline at end of file
diff --git a/Directory.Build.props b/Directory.Build.props
index 5fe723a5..82e632dd 100644
--- a/Directory.Build.props
+++ b/Directory.Build.props
@@ -22,10 +22,10 @@
LICENSE
true
assets/icon.png
- https://github.com/Stillpoint-Software/PostgresTest/releases/latest
- https://github.com/Stillpoint-Software/PostgresTest
+ https://github.com/Stillpoint-Software/hyperbee.json/releases/latest
+ https://github.com/Stillpoint-Software/hyperbee.json
git
- https://github.com/Stillpoint-Software/PostgresTest
+ https://stillpoint-software.github.io/hyperbee.json/
@@ -40,9 +40,15 @@
PackagePath="\"
Link="LICENSE" />
-
+
enable
- net10.0
+
+ net10.0;net9.0;net8.0
+
+
+
+
+ $(NoWarn);MSTEST0001
diff --git a/Directory.Packages.props b/Directory.Packages.props
index 5a1423a9..2e483172 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -1,7 +1,12 @@
-
- true
-
+
+
+ true
+
+ $(NoWarn);NU1608
+
+
+
@@ -27,6 +32,20 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Hyperbee.Json.sln.DotSettings b/Hyperbee.Json.sln.DotSettings
index 441eaa58..6a445907 100644
--- a/Hyperbee.Json.sln.DotSettings
+++ b/Hyperbee.Json.sln.DotSettings
@@ -37,6 +37,7 @@
<Policy Inspect="True" Prefix="" Suffix="" Style="AaBb"><ExtraRule Prefix="_" Suffix="" Style="aaBb" /></Policy>
<Policy Inspect="True" Prefix="" Suffix="" Style="AaBb"><ExtraRule Prefix="I" Suffix="" Style="AaBb" /></Policy>
<Policy><Descriptor Staticness="Static" AccessRightKinds="Private" Description="Static readonly fields (private)"><ElementKinds><Kind Name="READONLY_FIELD" /></ElementKinds></Descriptor><Policy Inspect="True" Prefix="" Suffix="" Style="AaBb"><ExtraRule Prefix="__" Suffix="" Style="aaBb" /></Policy></Policy>
+ <Policy><Descriptor Staticness="Any" AccessRightKinds="Any" Description="Type parameters"><ElementKinds><Kind Name="TYPE_PARAMETER" /></ElementKinds></Descriptor><Policy Inspect="True" WarnAboutPrefixesAndSuffixes="False" Prefix="" Suffix="" Style="AA_BB"><ExtraRule Prefix="" Suffix="" Style="Aa_bb" /></Policy></Policy>
<Policy><Descriptor Staticness="Instance" AccessRightKinds="Private" Description="Instance fields (private)"><ElementKinds><Kind Name="FIELD" /><Kind Name="READONLY_FIELD" /></ElementKinds></Descriptor><Policy Inspect="True" Prefix="_" Suffix="" Style="aaBb"><ExtraRule Prefix="" Suffix="" Style="AaBb" /></Policy></Policy>
<Policy><Descriptor Staticness="Instance" AccessRightKinds="Protected, ProtectedInternal, Internal, Public, PrivateProtected" Description="Instance fields (not private)"><ElementKinds><Kind Name="FIELD" /><Kind Name="READONLY_FIELD" /></ElementKinds></Descriptor><Policy Inspect="True" Prefix="" Suffix="" Style="AaBb"><ExtraRule Prefix="_" Suffix="" Style="aaBb" /></Policy></Policy>
<Policy><Descriptor Staticness="Static" AccessRightKinds="Protected, ProtectedInternal, Internal, Public, PrivateProtected" Description="Static fields (not private)"><ElementKinds><Kind Name="FIELD" /></ElementKinds></Descriptor><Policy Inspect="True" Prefix="" Suffix="" Style="AaBb"><ExtraRule Prefix="_" Suffix="" Style="aaBb" /></Policy></Policy>
diff --git a/NuGet.Config b/NuGet.Config
new file mode 100644
index 00000000..45631a81
--- /dev/null
+++ b/NuGet.Config
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/docs/.todo.md b/docs/.todo.md
deleted file mode 100644
index fc29bea6..00000000
--- a/docs/.todo.md
+++ /dev/null
@@ -1,4 +0,0 @@
-# Things TODO
-
-- Reenable CodeQL for the project once there is support for .NET 9
-
\ No newline at end of file
diff --git a/docs/docs.projitems b/docs/docs.projitems
index ab61f86f..729bacdf 100644
--- a/docs/docs.projitems
+++ b/docs/docs.projitems
@@ -9,7 +9,6 @@
docs
-
diff --git a/src/Hyperbee.Json/Dynamic/DynamicJsonElement.cs b/src/Hyperbee.Json/Dynamic/DynamicJsonElement.cs
index c153d4f7..82bdd512 100644
--- a/src/Hyperbee.Json/Dynamic/DynamicJsonElement.cs
+++ b/src/Hyperbee.Json/Dynamic/DynamicJsonElement.cs
@@ -43,7 +43,7 @@ public override bool TryGetIndex( GetIndexBinder binder, object[] indexes, out o
return true;
}
- result = null;
+ result = null!;
return false;
}
@@ -60,7 +60,7 @@ public override bool TryGetMember( GetMemberBinder binder, out object result )
}
}
- result = null;
+ result = null!;
return false;
}
@@ -74,7 +74,7 @@ public override bool TryInvokeMember( InvokeMemberBinder binder, object[] args,
return true;
}
- result = null;
+ result = null!;
return false;
}
diff --git a/src/Hyperbee.Json/Dynamic/DynamicJsonNode.cs b/src/Hyperbee.Json/Dynamic/DynamicJsonNode.cs
index cebda493..a144ee02 100644
--- a/src/Hyperbee.Json/Dynamic/DynamicJsonNode.cs
+++ b/src/Hyperbee.Json/Dynamic/DynamicJsonNode.cs
@@ -39,7 +39,7 @@ public override bool TryGetIndex( GetIndexBinder binder, object[] indexes, out o
return true;
}
- result = null;
+ result = null!;
return false;
}
@@ -57,7 +57,7 @@ public override bool TryGetMember( GetMemberBinder binder, out object result )
result = new DynamicJsonNode( ref arrayValue );
return true;
default:
- result = null;
+ result = null!;
return false;
}
}
@@ -86,7 +86,7 @@ public override bool TryInvokeMember( InvokeMemberBinder binder, object[] args,
return true;
}
- result = null;
+ result = null!;
return false;
}
diff --git a/src/Hyperbee.Json/Hyperbee.Json.csproj b/src/Hyperbee.Json/Hyperbee.Json.csproj
index 3a6111e4..5f881abf 100644
--- a/src/Hyperbee.Json/Hyperbee.Json.csproj
+++ b/src/Hyperbee.Json/Hyperbee.Json.csproj
@@ -1,21 +1,20 @@
-
- true
- Stillpoint Software, Inc.
- Hyperbee.Json
- README.md
- json-path;jsonpath;json-pointer;jsonpointer;json-patch;jsonpatch;query;path;patch;diff;json;rfc9535;rfc6901;rfc6902
- icon.png
- https://stillpoint-software.github.io/hyperbee.json/
- LICENSE
- Stillpoint Software, Inc.
- Hyperbee Json
- A high-performance JSON library for System.Text.Json JsonElement and JsonNode, providing robust support for JSONPath, JsonPointer, JsonPatch, and JsonDiff.
- https://github.com/Stillpoint-Software/Hyperbee.Json
- git
- https://github.com/Stillpoint-Software/Hyperbee.Json/releases/latest
-
-
+
+true
+Stillpoint Software, Inc.
+ Hyperbee.Json
+ README.md
+ json-path;jsonpath;json-pointer;jsonpointer;json-patch;jsonpatch;query;path;patch;diff;json;rfc9535;rfc6901;rfc6902
+ icon.png
+ https://stillpoint-software.github.io/hyperbee.json/
+ LICENSE
+ Stillpoint Software, Inc.
+ Hyperbee Json
+ A high-performance JSON library for System.Text.Json JsonElement and JsonNode, providing robust support for JSONPath, JsonPointer, JsonPatch, and JsonDiff.
+ https://github.com/Stillpoint-Software/Hyperbee.Json
+ git
+ https://github.com/Stillpoint-Software/Hyperbee.Json/releases/latest
+
<_Parameter1>$(AssemblyName).Tests
diff --git a/src/Hyperbee.Json/Path/Filters/Parser/Expressions/FunctionExpressionFactory.cs b/src/Hyperbee.Json/Path/Filters/Parser/Expressions/FunctionExpressionFactory.cs
index 8b600076..a5ab26aa 100644
--- a/src/Hyperbee.Json/Path/Filters/Parser/Expressions/FunctionExpressionFactory.cs
+++ b/src/Hyperbee.Json/Path/Filters/Parser/Expressions/FunctionExpressionFactory.cs
@@ -8,7 +8,7 @@ internal class FunctionExpressionFactory : IExpressionFactory
public static bool TryGetExpression( ref ParserState state, out Expression expression, out CompareConstraint compareConstraint, ITypeDescriptor descriptor )
{
compareConstraint = CompareConstraint.None;
- expression = null;
+ expression = null!;
if ( state.Item.IsEmpty || !char.IsLetter( state.Item[0] ) )
{
diff --git a/src/Hyperbee.Json/Path/Filters/Parser/Expressions/JsonExpressionFactory.cs b/src/Hyperbee.Json/Path/Filters/Parser/Expressions/JsonExpressionFactory.cs
index 8727d2e6..db5548b7 100644
--- a/src/Hyperbee.Json/Path/Filters/Parser/Expressions/JsonExpressionFactory.cs
+++ b/src/Hyperbee.Json/Path/Filters/Parser/Expressions/JsonExpressionFactory.cs
@@ -14,7 +14,7 @@ public static bool TryGetExpression( ref ParserState state, out Expressio
if ( !TryParseNode( descriptor.NodeActions, state.Item, out var node ) )
{
- expression = null;
+ expression = null!;
return false;
}
diff --git a/src/Hyperbee.Json/Path/Filters/Parser/Expressions/NotExpressionFactory.cs b/src/Hyperbee.Json/Path/Filters/Parser/Expressions/NotExpressionFactory.cs
index 8f016e23..04cb8dcd 100644
--- a/src/Hyperbee.Json/Path/Filters/Parser/Expressions/NotExpressionFactory.cs
+++ b/src/Hyperbee.Json/Path/Filters/Parser/Expressions/NotExpressionFactory.cs
@@ -8,7 +8,7 @@ internal class NotExpressionFactory : IExpressionFactory
public static bool TryGetExpression( ref ParserState state, out Expression expression, out CompareConstraint compareConstraint, ITypeDescriptor _ = null )
{
compareConstraint = CompareConstraint.None;
- expression = null;
+ expression = null!;
return state.Operator == Operator.Not;
}
diff --git a/src/Hyperbee.Json/Path/Filters/Parser/Expressions/ParenExpressionFactory.cs b/src/Hyperbee.Json/Path/Filters/Parser/Expressions/ParenExpressionFactory.cs
index ac7363c0..cc19e33b 100644
--- a/src/Hyperbee.Json/Path/Filters/Parser/Expressions/ParenExpressionFactory.cs
+++ b/src/Hyperbee.Json/Path/Filters/Parser/Expressions/ParenExpressionFactory.cs
@@ -11,7 +11,7 @@ public static bool TryGetExpression( ref ParserState state, out Expressio
if ( state.Operator != Operator.OpenParen || !state.Item.IsEmpty )
{
- expression = null;
+ expression = null!;
return false;
}
diff --git a/src/Hyperbee.Json/Path/Filters/Parser/Expressions/SelectExpressionFactory.cs b/src/Hyperbee.Json/Path/Filters/Parser/Expressions/SelectExpressionFactory.cs
index 298d720c..1ca0cf89 100644
--- a/src/Hyperbee.Json/Path/Filters/Parser/Expressions/SelectExpressionFactory.cs
+++ b/src/Hyperbee.Json/Path/Filters/Parser/Expressions/SelectExpressionFactory.cs
@@ -15,7 +15,7 @@ public static bool TryGetExpression( ref ParserState state, out Expressio
if ( item.IsEmpty || item[0] != '$' && item[0] != '@' )
{
- expression = null;
+ expression = null!;
return false;
}
diff --git a/src/Hyperbee.Json/Path/Filters/Parser/ExtensionFunction.cs b/src/Hyperbee.Json/Path/Filters/Parser/ExtensionFunction.cs
index eba5df1b..384ae5de 100644
--- a/src/Hyperbee.Json/Path/Filters/Parser/ExtensionFunction.cs
+++ b/src/Hyperbee.Json/Path/Filters/Parser/ExtensionFunction.cs
@@ -22,7 +22,7 @@ protected ExtensionFunction( MethodInfo methodInfo, CompareConstraint compareCon
internal Expression GetExpression( ref ParserState state )
{
var arguments = new Expression[_argumentCount];
- var expectNormalized = CompareConstraint.HasFlag( CompareConstraint.ExpectNormalized );
+ var expectNormalized = (CompareConstraint & CompareConstraint.ExpectNormalized) != 0;
for ( var i = 0; i < _argumentCount; i++ )
{
diff --git a/src/Hyperbee.Json/Path/Filters/Parser/FilterParser.cs b/src/Hyperbee.Json/Path/Filters/Parser/FilterParser.cs
index 4354f917..0210e075 100644
--- a/src/Hyperbee.Json/Path/Filters/Parser/FilterParser.cs
+++ b/src/Hyperbee.Json/Path/Filters/Parser/FilterParser.cs
@@ -63,7 +63,6 @@ internal static Expression Parse( ref ParserState state ) // recursion entrypoin
{
MoveNext( ref state );
items.Enqueue( GetExprItem( ref state ) ); // may cause recursion
-
} while ( state.IsParsing );
// check for paren mismatch
@@ -153,11 +152,24 @@ static bool IsFinished( in ParserState state, char ch, ref int itemEnd )
private static void MoveNextOperator( ref ParserState state ) // move to the next operator
{
if ( state.Operator.IsLogical() || state.Operator.IsComparison() || state.Operator.IsMath() )
- {
return;
- }
- if ( !state.IsParsing )
+ // Determine if we should stop looking for an operator.
+ //
+ // When IsParsing is false (Previous == TerminalCharacter), we've hit a potential stopping point.
+ // However, ')' as a terminal character is ambiguous - it could be:
+ // 1. A function's closing paren: `length(@.x)` - should continue to find `> 10` in `(length(@.x) > 10)`
+ // 2. The outer expression's closing paren - should stop
+ // 3. A function argument's closing paren - should stop
+ //
+ // We return early (stop) when:
+ // - TerminalCharacter is not ')' (e.g., ',' is unambiguous), OR
+ // - ParenDepth == 0 (we're at the outermost level, so ')' closes the expression), OR
+ // - IsArgument is true (we're parsing a function argument, so ')' is definitely ours)
+ //
+ // Otherwise, we fall through to the while loop to continue scanning for operators.
+
+ if ( !state.IsParsing && (state.TerminalCharacter != ArgClose || state.ParenDepth == 0 || state.IsArgument) )
{
state.Operator = Operator.NonOperator;
return;
@@ -174,6 +186,7 @@ private static void MoveNextOperator( ref ParserState state ) // move to the nex
private static void NextCharacter( ref ParserState state, int start, out char nextChar, ref char? quoteChar )
{
+ // Read next character
nextChar = state.Buffer[state.Pos++];
// Handle escape characters within quotes
@@ -438,25 +451,30 @@ private static void ThrowIfInvalidCompare( in ParserState state, ExprItem left,
private static void ThrowIfLiteralInvalidCompare( in ParserState state, ExprItem left, ExprItem right )
{
+ const CompareConstraint literalMustCompare = CompareConstraint.Literal | CompareConstraint.MustCompare;
+
if ( state.IsArgument || left.Operator.IsMath() )
return;
- if ( left.CompareConstraint.HasFlag( CompareConstraint.Literal | CompareConstraint.MustCompare ) && !left.Operator.IsComparison() )
+ if ( (left.CompareConstraint & literalMustCompare) == literalMustCompare && !left.Operator.IsComparison() )
throw new NotSupportedException( $"Unsupported literal without comparison: {state.Buffer.ToString()}." );
- if ( right != null && right.CompareConstraint.HasFlag( CompareConstraint.Literal | CompareConstraint.MustCompare ) && !left.Operator.IsComparison() )
+ if ( right != null && (right.CompareConstraint & literalMustCompare) == literalMustCompare && !left.Operator.IsComparison() )
throw new NotSupportedException( $"Unsupported literal without comparison: {state.Buffer.ToString()}." );
}
private static void ThrowIfFunctionInvalidCompare( in ParserState state, ExprItem item )
{
+ const CompareConstraint functionMustCompare = CompareConstraint.Function | CompareConstraint.MustCompare;
+ const CompareConstraint functionMustNotCompare = CompareConstraint.Function | CompareConstraint.MustNotCompare;
+
if ( state.IsArgument )
return;
- if ( item.CompareConstraint.HasFlag( CompareConstraint.Function | CompareConstraint.MustCompare ) && !item.Operator.IsComparison() )
+ if ( (item.CompareConstraint & functionMustCompare) == functionMustCompare && !item.Operator.IsComparison() )
throw new NotSupportedException( $"Function must compare: {state.Buffer.ToString()}." );
- if ( item.CompareConstraint.HasFlag( CompareConstraint.Function | CompareConstraint.MustNotCompare ) && item.Operator.IsComparison() )
+ if ( (item.CompareConstraint & functionMustNotCompare) == functionMustNotCompare && item.Operator.IsComparison() )
throw new NotSupportedException( $"Function must not compare: {state.Buffer.ToString()}." );
}
diff --git a/src/Hyperbee.Json/Path/Filters/Parser/Operator.cs b/src/Hyperbee.Json/Path/Filters/Parser/Operator.cs
index 0343fee5..ad1fcea7 100644
--- a/src/Hyperbee.Json/Path/Filters/Parser/Operator.cs
+++ b/src/Hyperbee.Json/Path/Filters/Parser/Operator.cs
@@ -46,8 +46,9 @@ public enum Operator
internal static class OperatorExtensions
{
- public static bool IsNonOperator( this Operator op ) => op.HasFlag( Operator.NonOperator );
- public static bool IsComparison( this Operator op ) => op.HasFlag( Operator.Comparison );
- public static bool IsLogical( this Operator op ) => op.HasFlag( Operator.Logical );
- public static bool IsMath( this Operator op ) => op.HasFlag( Operator.Math );
+ // Use direct bitwise operations instead of HasFlag() to avoid boxing allocations
+ public static bool IsNonOperator( this Operator op ) => (op & Operator.NonOperator) != 0;
+ public static bool IsComparison( this Operator op ) => (op & Operator.Comparison) != 0;
+ public static bool IsLogical( this Operator op ) => (op & Operator.Logical) != 0;
+ public static bool IsMath( this Operator op ) => (op & Operator.Math) != 0;
}
diff --git a/src/Hyperbee.Json/Path/Filters/Parser/ValueTypeComparer.cs b/src/Hyperbee.Json/Path/Filters/Parser/ValueTypeComparer.cs
index 7c6d780d..2be9910e 100644
--- a/src/Hyperbee.Json/Path/Filters/Parser/ValueTypeComparer.cs
+++ b/src/Hyperbee.Json/Path/Filters/Parser/ValueTypeComparer.cs
@@ -148,10 +148,14 @@ static IEnumerable EnumerateChildren( IValueAccessor accessor, TNo
static bool Contains( IValueTypeComparer comparer, IValueAccessor accessor, IValueType left, TNode rightNode )
{
- return EnumerateChildren( accessor, rightNode )
- .Select( rightChild => GetComparand( accessor, rightChild ) )
- .Select( comparand => comparer.Compare( left, comparand, Operator.Equals ) )
- .Any( result => result == 0 );
+ foreach ( var rightChild in EnumerateChildren( accessor, rightNode ) )
+ {
+ var comparand = GetComparand( accessor, rightChild );
+ if ( comparer.Compare( left, comparand, Operator.Equals ) == 0 )
+ return true;
+ }
+
+ return false;
}
static IValueType GetComparand( IValueAccessor accessor, TNode childValue )
@@ -290,10 +294,10 @@ private static bool TryGetValue( IValueAccessor accessor, TNode node, out
{
nodeType = itemValue switch
{
- string itemString => Scalar.Value( itemString ),
- bool itemBool => Scalar.Value( itemBool ),
- float itemFloat => Scalar.Value( itemFloat ),
- int itemInt => Scalar.Value( itemInt ),
+ string itemString => new ScalarValue( itemString ),
+ bool itemBool => new ScalarValue( itemBool ),
+ float itemFloat => new ScalarValue( itemFloat ),
+ int itemInt => new ScalarValue( itemInt ),
null => Scalar.Null,
_ => throw new NotSupportedException( "Unsupported value type." )
};
diff --git a/src/Hyperbee.Json/Path/JsonPath.cs b/src/Hyperbee.Json/Path/JsonPath.cs
index 205e3a90..6f43c9a4 100644
--- a/src/Hyperbee.Json/Path/JsonPath.cs
+++ b/src/Hyperbee.Json/Path/JsonPath.cs
@@ -45,6 +45,8 @@
namespace Hyperbee.Json.Path;
+#pragma warning disable CS1717
+
internal static class IndexHelper
{
private const int LookupLength = 64;
@@ -521,38 +523,6 @@ public void Dispose()
_disposed = true;
}
}
-
- //private sealed class NodeArgsStack( int capacity = 8 )
- //{
- // [DebuggerBrowsable( DebuggerBrowsableState.RootHidden )]
- // private readonly Stack _stack = new(capacity);
-
- // [MethodImpl( MethodImplOptions.AggressiveInlining )]
- // public void Push( in TNode parent, in TNode value, string key, in JsonSegment segment, NodeFlags flags = NodeFlags.Default )
- // {
- // _stack.Push( new NodeArgs( parent, value, key, segment, flags ) );
- // }
-
- // [MethodImpl( MethodImplOptions.AggressiveInlining )]
- // public void Push( in TNode parent, in TNode value, int index, in JsonSegment segment, NodeFlags flags = NodeFlags.Default )
- // {
- // _stack.Push( new NodeArgs( parent, value, IndexHelper.GetIndexString( index ), segment, flags ) );
- // }
-
- // public void PushMany( in TNode parent, in IEnumerable<(TNode Value, string Key)> items, in JsonSegment segment, NodeFlags flags = NodeFlags.Default )
- // {
- // foreach ( var (value, key) in items )
- // {
- // _stack.Push( new NodeArgs( parent, value, key, segment, flags ) );
- // }
- // }
-
- // [MethodImpl( MethodImplOptions.AggressiveInlining )]
- // public bool TryPop( out NodeArgs args )
- // {
- // return _stack.TryPop( out args );
- // }
- //}
}
diff --git a/test/Hyperbee.Json.Benchmark/Config.cs b/test/Hyperbee.Json.Benchmark/Helpers/Config.cs
similarity index 98%
rename from test/Hyperbee.Json.Benchmark/Config.cs
rename to test/Hyperbee.Json.Benchmark/Helpers/Config.cs
index 433ac8af..f500405b 100644
--- a/test/Hyperbee.Json.Benchmark/Config.cs
+++ b/test/Hyperbee.Json.Benchmark/Helpers/Config.cs
@@ -6,7 +6,7 @@
using BenchmarkDotNet.Reports;
using BenchmarkDotNet.Validators;
-namespace Hyperbee.Json.Benchmark;
+namespace Hyperbee.Json.Benchmark.Helpers;
public class Config : ManualConfig
{
diff --git a/test/Hyperbee.Json.Benchmark/FastestToSlowestByParamOrderer.cs b/test/Hyperbee.Json.Benchmark/Helpers/FastestToSlowestByParamOrderer.cs
similarity index 97%
rename from test/Hyperbee.Json.Benchmark/FastestToSlowestByParamOrderer.cs
rename to test/Hyperbee.Json.Benchmark/Helpers/FastestToSlowestByParamOrderer.cs
index cfde22b7..e10af8a3 100644
--- a/test/Hyperbee.Json.Benchmark/FastestToSlowestByParamOrderer.cs
+++ b/test/Hyperbee.Json.Benchmark/Helpers/FastestToSlowestByParamOrderer.cs
@@ -4,7 +4,7 @@
using BenchmarkDotNet.Reports;
using BenchmarkDotNet.Running;
-namespace Hyperbee.Json.Benchmark;
+namespace Hyperbee.Json.Benchmark.Helpers;
public class FastestToSlowestByParamOrderer : IOrderer
{
diff --git a/test/Hyperbee.Json.Benchmark/FilterExpressionParserEvaluator.cs b/test/Hyperbee.Json.Benchmark/Helpers/FilterExpressionBenchmark.cs
similarity index 84%
rename from test/Hyperbee.Json.Benchmark/FilterExpressionParserEvaluator.cs
rename to test/Hyperbee.Json.Benchmark/Helpers/FilterExpressionBenchmark.cs
index 34630b8d..0f7f44ab 100644
--- a/test/Hyperbee.Json.Benchmark/FilterExpressionParserEvaluator.cs
+++ b/test/Hyperbee.Json.Benchmark/Helpers/FilterExpressionBenchmark.cs
@@ -3,9 +3,9 @@
using BenchmarkDotNet.Attributes;
using Hyperbee.Json.Path.Filters.Parser;
-namespace Hyperbee.Json.Benchmark;
+namespace Hyperbee.Json.Benchmark.Helpers;
-public class FilterExpressionParserEvaluator
+public class FilterExpressionBenchmark
{
[Params( "(\"world\" == 'world') && (true || false)" )]
public string Filter;
diff --git a/test/Hyperbee.Json.Benchmark/JsonPathMarkdownExporter.cs b/test/Hyperbee.Json.Benchmark/Helpers/JsonPathMarkdownExporter.cs
similarity index 97%
rename from test/Hyperbee.Json.Benchmark/JsonPathMarkdownExporter.cs
rename to test/Hyperbee.Json.Benchmark/Helpers/JsonPathMarkdownExporter.cs
index c5feb1dd..33fd4e4d 100644
--- a/test/Hyperbee.Json.Benchmark/JsonPathMarkdownExporter.cs
+++ b/test/Hyperbee.Json.Benchmark/Helpers/JsonPathMarkdownExporter.cs
@@ -2,7 +2,7 @@
using BenchmarkDotNet.Loggers;
using BenchmarkDotNet.Reports;
-namespace Hyperbee.Json.Benchmark;
+namespace Hyperbee.Json.Benchmark.Helpers;
// Custom exporter that groups tests by filter and displays only specified columns
public class JsonPathMarkdownExporter : ExporterBase
@@ -27,7 +27,7 @@ public override void ExportToLog( Summary summary, ILogger logger )
}
logger.WriteLine();
- foreach ( string infoLine in summary.HostEnvironmentInfo.ToFormattedString() )
+ foreach ( var infoLine in summary.HostEnvironmentInfo.ToFormattedString() )
{
logger.WriteLineInfo( infoLine );
}
@@ -80,7 +80,7 @@ private void PrintTable( Summary summary, ILogger logger, SummaryStyle style )
PrintHeader( columns, logger );
- int rowCounter = 0;
+ var rowCounter = 0;
foreach ( var line in table.FullContent )
{
diff --git a/test/Hyperbee.Json.Benchmark/Hyperbee.Json.Benchmark.csproj b/test/Hyperbee.Json.Benchmark/Hyperbee.Json.Benchmark.csproj
index 198d5cdb..9518f326 100644
--- a/test/Hyperbee.Json.Benchmark/Hyperbee.Json.Benchmark.csproj
+++ b/test/Hyperbee.Json.Benchmark/Hyperbee.Json.Benchmark.csproj
@@ -1,9 +1,11 @@
-
+
-
- Exe
- false
-
+
+
+ net10.0
+ Exe
+ false
+
@@ -14,7 +16,7 @@
-
+
diff --git a/test/Hyperbee.Json.Benchmark/JsonPathParseAndSelectEvaluator.cs b/test/Hyperbee.Json.Benchmark/JsonPathBenchmark.cs
similarity index 79%
rename from test/Hyperbee.Json.Benchmark/JsonPathParseAndSelectEvaluator.cs
rename to test/Hyperbee.Json.Benchmark/JsonPathBenchmark.cs
index 875d3767..19e9cd91 100644
--- a/test/Hyperbee.Json.Benchmark/JsonPathParseAndSelectEvaluator.cs
+++ b/test/Hyperbee.Json.Benchmark/JsonPathBenchmark.cs
@@ -10,34 +10,60 @@
namespace Hyperbee.Json.Benchmark;
-public class JsonPathParseAndSelectEvaluator
+public class JsonPathBenchmark
{
+ /*
[Params(
+| | `$..* First()`
+ | `$..*`
+ | `$..price`
+ | `$.store.book[?(@.price == 8.99)]`
+ | `$.store.book[0]`
+ )]
+ */
+
+ [Params(
+ // Root and Wildcard
+ "$",
+ "$.store.*",
+ "$.store.* #First()", // Test Enumerable.First()
+
+ // Property and Index Access
+ "$.store.book[0]",
"$.store.book[0].title",
- "$.store.book[*].author",
- "$.store.book[?(@.price < 10)].title",
- "$.store.bicycle.color",
"$.store.book[*]",
+ "$.store.book[*].author",
+ "$.store.book['category','author']",
+
+ // Recursive Descent
"$.store..price",
"$..author",
- "$.store.book[?(@.price > 10 && @.price < 20)]",
- "$.store.book[?(@.category == 'fiction')]",
- "$.store.book[-1:]",
- "$.store.book[:2]",
- "$..book[0,1]",
"$..*",
"$..['bicycle','price']",
- "$..[?(@.price < 10)]",
+ "$..book[0,1]",
+ "$..book[?@.isbn]",
+
+ // Filters
+ "$.store.book[?(@.price < 10)].title",
+ "$.store.book[?(@.price > 10 && @.price < 20)]",
+ "$.store.book[?(@.category == 'fiction')]",
"$.store.book[?(@.author && @.title)]",
- "$.store.*",
- "$",
- "$.store.book[0]",
- "$..book[0]",
+ "$.store.book[?(@.price == 8.99)]",
+ "$..[?(@.price < 10)]",
+ "$..book[?@.price == 8.99 && @.category == 'fiction']",
+ "$.store.book[?(@.price < 10 || @.category == 'fiction')]",
+ "$.store.book[?(!@.isbn)]",
+ "$.store.book[?(length(@.title) > 10)]",
+
+
+ // Array Slices and Unions
+ "$.store.book[-1:]",
"$.store.book[0,1]",
- "$.store.book['category','author']",
- "$..book[?@.isbn]",
- "$.store.book[?@.price == 8.99]",
- "$..book[?@.price == 8.99 && @.category == 'fiction']"
+ "$.store.book[:2]",
+ "$.store.book[0:3:2]",
+
+ // Property Access (Direct)
+ "$.store.bicycle.color"
)]
public string Filter;
@@ -100,7 +126,7 @@ public void Setup()
public (string, bool) GetFilter()
{
- const string First = " ::First()";
+ const string First = " #First()";
return Filter.EndsWith( First )
? (Filter[..^First.Length], true)
@@ -119,14 +145,12 @@ private void Consume( IEnumerable select, bool takeFirst )
public void Hyperbee_JsonElement()
{
var (filter, first) = GetFilter();
-
- var element = JsonDocument.Parse( Document ).RootElement;
- var select = element.Select( filter );
+ var select = _element.Select( filter );
Consume( select, first );
}
- [Benchmark( Description = "Hyperbee.JsonNode" )]
+ //[Benchmark( Description = "Hyperbee.JsonNode" )]
public void Hyperbee_JsonNode()
{
var (filter, first) = GetFilter();
@@ -137,7 +161,7 @@ public void Hyperbee_JsonNode()
Consume( select, first );
}
- [Benchmark( Description = "Newtonsoft.JObject" )]
+ //[Benchmark( Description = "Newtonsoft.JObject" )]
public void Newtonsoft_JObject()
{
var (filter, first) = GetFilter();
@@ -148,7 +172,7 @@ public void Newtonsoft_JObject()
Consume( select, first );
}
- [Benchmark( Description = "JsonEverything.JsonNode" )]
+ //[Benchmark( Description = "JsonEverything.JsonNode" )]
public void JsonEverything_JsonNode()
{
var (filter, first) = GetFilter();
@@ -160,7 +184,7 @@ public void JsonEverything_JsonNode()
Consume( select, first );
}
- [Benchmark( Description = "JsonCons.JsonElement" )]
+ //[Benchmark( Description = "JsonCons.JsonElement" )]
public void JsonCons_JsonElement()
{
var (filter, first) = GetFilter();
@@ -171,15 +195,4 @@ public void JsonCons_JsonElement()
Consume( select, first );
}
-
- [Benchmark( Description = "JsonCraft.JsonElement" )]
- public void JsonCraft_JsonElement()
- {
- var (filter, first) = GetFilter();
-
- var element = JsonDocument.Parse( Document ).RootElement;
- var select = JsonCraft.JsonPath.JsonExtensions.SelectElements( element, filter );
-
- Consume( select, first );
- }
}
diff --git a/test/Hyperbee.Json.Benchmark/Program.cs b/test/Hyperbee.Json.Benchmark/Program.cs
index 6da9a234..caa53d0f 100644
--- a/test/Hyperbee.Json.Benchmark/Program.cs
+++ b/test/Hyperbee.Json.Benchmark/Program.cs
@@ -1,5 +1,5 @@
//NOTE: Should be run with `dotnet run -c release` in the project folder
using BenchmarkDotNet.Running;
-using Hyperbee.Json.Benchmark;
+using Hyperbee.Json.Benchmark.Helpers;
BenchmarkSwitcher.FromAssembly( typeof( Program ).Assembly ).Run( args, new Config() );
diff --git a/test/Hyperbee.Json.Benchmark/benchmark/results/Hyperbee.Json.Benchmark.JsonPathBenchmark-report-jsonpath.md b/test/Hyperbee.Json.Benchmark/benchmark/results/Hyperbee.Json.Benchmark.JsonPathBenchmark-report-jsonpath.md
new file mode 100644
index 00000000..bd51b2b8
--- /dev/null
+++ b/test/Hyperbee.Json.Benchmark/benchmark/results/Hyperbee.Json.Benchmark.JsonPathBenchmark-report-jsonpath.md
@@ -0,0 +1,98 @@
+```
+
+BenchmarkDotNet v0.15.8, Windows 11 (10.0.26200.7462/25H2/2025Update/HudsonValley2)
+Intel Core i9-9980HK CPU 2.40GHz, 1 CPU, 16 logical and 8 physical cores
+.NET SDK 10.0.101
+ [Host] : .NET 10.0.1 (10.0.1, 10.0.125.57005), X64 RyuJIT x86-64-v3
+ ShortRun : .NET 10.0.1 (10.0.1, 10.0.125.57005), X64 RyuJIT x86-64-v3
+
+
+ | Method | Mean | Error | StdDev | Allocated
+ | :-------------------- | -----------: | -----------: | ----------: | ---------:
+ | `$..[?(@.price < 10)]`
+ | Hyperbee.JsonElement | 6,994.02 ns | 3,956.98 ns | 216.896 ns | 14056 B
+ | | | | |
+ | `$..['bicycle','price']`
+ | Hyperbee.JsonElement | 2,328.61 ns | 1,084.27 ns | 59.432 ns | 3072 B
+ | | | | |
+ | `$..*`
+ | Hyperbee.JsonElement | 1,668.53 ns | 1,533.68 ns | 84.066 ns | 4432 B
+ | | | | |
+ | `$..author`
+ | Hyperbee.JsonElement | 1,693.51 ns | 1,479.36 ns | 81.089 ns | 3056 B
+ | | | | |
+ | `$..book[?@.isbn]`
+ | Hyperbee.JsonElement | 2,478.21 ns | 2,475.52 ns | 135.692 ns | 4120 B
+ | | | | |
+ | `$..book[?@.price == 8.99 && @.category == 'fiction']`
+ | Hyperbee.JsonElement | 3,911.73 ns | 5,909.55 ns | 323.922 ns | 6312 B
+ | | | | |
+ | `$..book[0,1]`
+ | Hyperbee.JsonElement | 1,724.88 ns | 561.96 ns | 30.803 ns | 3056 B
+ | | | | |
+ | `$.store..price`
+ | Hyperbee.JsonElement | 1,568.51 ns | 179.48 ns | 9.838 ns | 2680 B
+ | | | | |
+ | `$.store.* #First()`
+ | Hyperbee.JsonElement | 440.89 ns | 43.81 ns | 2.401 ns | 752 B
+ | | | | |
+ | `$.store.*`
+ | Hyperbee.JsonElement | 452.31 ns | 124.85 ns | 6.843 ns | 712 B
+ | | | | |
+ | `$.store.bicycle.color`
+ | Hyperbee.JsonElement | 159.90 ns | 113.32 ns | 6.212 ns | 80 B
+ | | | | |
+ | `$.store.book[-1:]`
+ | Hyperbee.JsonElement | 428.00 ns | 934.88 ns | 51.244 ns | 296 B
+ | | | | |
+ | `$.store.book[:2]`
+ | Hyperbee.JsonElement | 420.74 ns | 397.59 ns | 21.793 ns | 296 B
+ | | | | |
+ | `$.store.book[?(!@.isbn)]`
+ | Hyperbee.JsonElement | 1,160.92 ns | 337.16 ns | 18.481 ns | 1360 B
+ | | | | |
+ | `$.store.book[?(@.author && @.title)]`
+ | Hyperbee.JsonElement | 1,646.82 ns | 413.39 ns | 22.659 ns | 2112 B
+ | | | | |
+ | `$.store.book[?(@.category == 'fiction')]`
+ | Hyperbee.JsonElement | 1,509.30 ns | 596.57 ns | 32.700 ns | 2272 B
+ | | | | |
+ | `$.store.book[?(@.price < 10 || @.category == 'fiction')]`
+ | Hyperbee.JsonElement | 2,475.60 ns | 1,523.46 ns | 83.506 ns | 3520 B
+ | | | | |
+ | `$.store.book[?(@.price < 10)].title`
+ | Hyperbee.JsonElement | 1,700.97 ns | 513.45 ns | 28.144 ns | 2288 B
+ | | | | |
+ | `$.store.book[?(@.price == 8.99)]`
+ | Hyperbee.JsonElement | 1,493.31 ns | 277.47 ns | 15.209 ns | 2080 B
+ | | | | |
+ | `$.store.book[?(@.price > 10 && @.price < 20)]`
+ | Hyperbee.JsonElement | 2,276.48 ns | 1,976.86 ns | 108.358 ns | 3328 B
+ | | | | |
+ | `$.store.book[?(length(@.title) > 10)]`
+ | Hyperbee.JsonElement | 1,719.12 ns | 329.32 ns | 18.051 ns | 2056 B
+ | | | | |
+ | `$.store.book['category','author']`
+ | Hyperbee.JsonElement | 1,140.57 ns | 523.85 ns | 28.714 ns | 504 B
+ | | | | |
+ | `$.store.book[*].author`
+ | Hyperbee.JsonElement | 1,011.08 ns | 451.88 ns | 24.769 ns | 960 B
+ | | | | |
+ | `$.store.book[*]`
+ | Hyperbee.JsonElement | 554.85 ns | 86.42 ns | 4.737 ns | 544 B
+ | | | | |
+ | `$.store.book[0,1]`
+ | Hyperbee.JsonElement | 482.44 ns | 685.33 ns | 37.565 ns | 296 B
+ | | | | |
+ | `$.store.book[0:3:2]`
+ | Hyperbee.JsonElement | 445.85 ns | 496.92 ns | 27.238 ns | 296 B
+ | | | | |
+ | `$.store.book[0].title`
+ | Hyperbee.JsonElement | 202.18 ns | 135.14 ns | 7.408 ns | 80 B
+ | | | | |
+ | `$.store.book[0]`
+ | Hyperbee.JsonElement | 159.31 ns | 127.33 ns | 6.979 ns | 80 B
+ | | | | |
+ | `$`
+ | Hyperbee.JsonElement | 33.32 ns | 14.04 ns | 0.770 ns | 56 B
+```
diff --git a/test/Hyperbee.Json.Tests/Hyperbee.Json.Tests.csproj b/test/Hyperbee.Json.Tests/Hyperbee.Json.Tests.csproj
index f3ad1727..caf652ae 100644
--- a/test/Hyperbee.Json.Tests/Hyperbee.Json.Tests.csproj
+++ b/test/Hyperbee.Json.Tests/Hyperbee.Json.Tests.csproj
@@ -1,8 +1,10 @@
-
- false
- Hyperbee.Json.Tests
-
+
+
+ false
+ true
+ Hyperbee.Json.Tests
+
diff --git a/test/Hyperbee.Json.Tests/Path/Parser/FilterParserTests.cs b/test/Hyperbee.Json.Tests/Path/Parser/FilterParserTests.cs
index 5b0c4ba3..bb6b60b1 100644
--- a/test/Hyperbee.Json.Tests/Path/Parser/FilterParserTests.cs
+++ b/test/Hyperbee.Json.Tests/Path/Parser/FilterParserTests.cs
@@ -122,6 +122,22 @@ public void ReturnExpectedResult_WhenUsingExpressionEvaluator( string filter, fl
Assert.AreEqual( expected, result );
}
+ [TestMethod]
+ [DataRow( "$.store.book[?(length(@.title) > 10)].title", "Sayings of the Century", typeof( JsonElement ) )]
+ [DataRow( "$.store.book[?(length(@.title) > 10)].title", "Sayings of the Century", typeof( JsonNode ) )]
+ public void ReturnExpectedResult_WhenUsingExpressionEvaluator( string filter, string expected, Type sourceType )
+ {
+ // arrange & act
+ var document = GetDocumentAdapter( sourceType );
+
+ // act
+ var matches = document.Select( filter ).ToArray();
+ var result = TestHelper.GetString( matches[0] );
+
+ // assert
+ Assert.AreEqual( expected, result );
+ }
+
[TestMethod]
[DataRow( "count(@.store.book) == 1", true, typeof( JsonElement ) )]
[DataRow( "count(@.store.book.*) == 4", true, typeof( JsonElement ) )]