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