Skip to content

Commit d738815

Browse files
committed
Refines filter parsing for function comparisons
Updates the filter parser to correctly handle function comparisons within JSONPath expressions. Ensures that functions requiring comparison (e.g., length(@.title)) are properly validated and throw exceptions when used as standalone expressions without a comparison operator. This change prevents invalid filter expressions while allowing valid expressions with comparison operators (e.g., length(@.title) > 10). Also includes benchmark updates and minor cleanup.
1 parent 2f714ca commit d738815

13 files changed

Lines changed: 196 additions & 92 deletions

File tree

docs/.todo.md

Lines changed: 0 additions & 4 deletions
This file was deleted.

docs/docs.projitems

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@
99
<Import_RootNamespace>docs</Import_RootNamespace>
1010
</PropertyGroup>
1111
<ItemGroup>
12-
<None Include="$(MSBuildThisFileDirectory).todo.md" />
1312
<None Include="$(MSBuildThisFileDirectory)additional-classes.md" />
1413
<None Include="$(MSBuildThisFileDirectory)index.md" />
1514
<None Include="$(MSBuildThisFileDirectory)jsonpatch.md" />

src/Hyperbee.Json/Path/Filters/Parser/FilterParser.cs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -453,7 +453,11 @@ private static void ThrowIfFunctionInvalidCompare( in ParserState state, ExprIte
453453
if ( state.IsArgument )
454454
return;
455455

456-
if ( item.CompareConstraint.HasFlag( CompareConstraint.Function | CompareConstraint.MustCompare ) && !item.Operator.IsComparison() )
456+
// Only throw if this function (which must be compared) is left as a standalone expression
457+
// at the end of the filter. This ensures that filters like [?(length(@.title))] are rejected,
458+
// but [?(length(@.title) > 10)] are allowed. We defer this check until EndOfBuffer to allow
459+
// the function to be merged with a comparison operator if present.
460+
if ( item.CompareConstraint.HasFlag( CompareConstraint.Function | CompareConstraint.MustCompare ) && !item.Operator.IsComparison() && state.EndOfBuffer )
457461
throw new NotSupportedException( $"Function must compare: {state.Buffer.ToString()}." );
458462

459463
if ( item.CompareConstraint.HasFlag( CompareConstraint.Function | CompareConstraint.MustNotCompare ) && item.Operator.IsComparison() )

src/Hyperbee.Json/Path/JsonPath.cs

Lines changed: 5 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,8 @@
4545

4646
namespace Hyperbee.Json.Path;
4747

48+
#pragma warning disable CS1717
49+
4850
internal static class IndexHelper
4951
{
5052
private const int LookupLength = 64;
@@ -158,9 +160,9 @@ private static IEnumerable<TNode> EnumerateMatches( TNode root, NodeArgs args, N
158160
var (parent, value, key, segmentNext, flags) = args;
159161

160162
ProcessArgs:
161-
// call node processor if it exists and the `key` is not null.
162-
// the key is null when a descent has re-pushed the descent target.
163-
// this should be safe to skip; we will see its values later.
163+
// call node processor if it exists and the `key` is not null.
164+
// the key is null when a descent has re-pushed the descent target.
165+
// this should be safe to skip; we will see its values later.
164166

165167
if ( key != null )
166168
processor?.Invoke( parent, value, key, segmentNext );
@@ -521,38 +523,6 @@ public void Dispose()
521523
_disposed = true;
522524
}
523525
}
524-
525-
//private sealed class NodeArgsStack( int capacity = 8 )
526-
//{
527-
// [DebuggerBrowsable( DebuggerBrowsableState.RootHidden )]
528-
// private readonly Stack<NodeArgs> _stack = new(capacity);
529-
530-
// [MethodImpl( MethodImplOptions.AggressiveInlining )]
531-
// public void Push( in TNode parent, in TNode value, string key, in JsonSegment segment, NodeFlags flags = NodeFlags.Default )
532-
// {
533-
// _stack.Push( new NodeArgs( parent, value, key, segment, flags ) );
534-
// }
535-
536-
// [MethodImpl( MethodImplOptions.AggressiveInlining )]
537-
// public void Push( in TNode parent, in TNode value, int index, in JsonSegment segment, NodeFlags flags = NodeFlags.Default )
538-
// {
539-
// _stack.Push( new NodeArgs( parent, value, IndexHelper.GetIndexString( index ), segment, flags ) );
540-
// }
541-
542-
// public void PushMany( in TNode parent, in IEnumerable<(TNode Value, string Key)> items, in JsonSegment segment, NodeFlags flags = NodeFlags.Default )
543-
// {
544-
// foreach ( var (value, key) in items )
545-
// {
546-
// _stack.Push( new NodeArgs( parent, value, key, segment, flags ) );
547-
// }
548-
// }
549-
550-
// [MethodImpl( MethodImplOptions.AggressiveInlining )]
551-
// public bool TryPop( out NodeArgs args )
552-
// {
553-
// return _stack.TryPop( out args );
554-
// }
555-
//}
556526
}
557527

558528

test/Hyperbee.Json.Benchmark/Config.cs renamed to test/Hyperbee.Json.Benchmark/Helpers/Config.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
using BenchmarkDotNet.Reports;
77
using BenchmarkDotNet.Validators;
88

9-
namespace Hyperbee.Json.Benchmark;
9+
namespace Hyperbee.Json.Benchmark.Helpers;
1010

1111
public class Config : ManualConfig
1212
{

test/Hyperbee.Json.Benchmark/FastestToSlowestByParamOrderer.cs renamed to test/Hyperbee.Json.Benchmark/Helpers/FastestToSlowestByParamOrderer.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
using BenchmarkDotNet.Reports;
55
using BenchmarkDotNet.Running;
66

7-
namespace Hyperbee.Json.Benchmark;
7+
namespace Hyperbee.Json.Benchmark.Helpers;
88

99
public class FastestToSlowestByParamOrderer : IOrderer
1010
{

test/Hyperbee.Json.Benchmark/FilterExpressionParserEvaluator.cs renamed to test/Hyperbee.Json.Benchmark/Helpers/FilterExpressionBenchmark.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,9 @@
33
using BenchmarkDotNet.Attributes;
44
using Hyperbee.Json.Path.Filters.Parser;
55

6-
namespace Hyperbee.Json.Benchmark;
6+
namespace Hyperbee.Json.Benchmark.Helpers;
77

8-
public class FilterExpressionParserEvaluator
8+
public class FilterExpressionBenchmark
99
{
1010
[Params( "(\"world\" == 'world') && (true || false)" )]
1111
public string Filter;

test/Hyperbee.Json.Benchmark/JsonPathMarkdownExporter.cs renamed to test/Hyperbee.Json.Benchmark/Helpers/JsonPathMarkdownExporter.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
using BenchmarkDotNet.Loggers;
33
using BenchmarkDotNet.Reports;
44

5-
namespace Hyperbee.Json.Benchmark;
5+
namespace Hyperbee.Json.Benchmark.Helpers;
66

77
// Custom exporter that groups tests by filter and displays only specified columns
88
public class JsonPathMarkdownExporter : ExporterBase
@@ -27,7 +27,7 @@ public override void ExportToLog( Summary summary, ILogger logger )
2727
}
2828

2929
logger.WriteLine();
30-
foreach ( string infoLine in summary.HostEnvironmentInfo.ToFormattedString() )
30+
foreach ( var infoLine in summary.HostEnvironmentInfo.ToFormattedString() )
3131
{
3232
logger.WriteLineInfo( infoLine );
3333
}
@@ -80,7 +80,7 @@ private void PrintTable( Summary summary, ILogger logger, SummaryStyle style )
8080

8181
PrintHeader( columns, logger );
8282

83-
int rowCounter = 0;
83+
var rowCounter = 0;
8484

8585
foreach ( var line in table.FullContent )
8686
{

test/Hyperbee.Json.Benchmark/Hyperbee.Json.Benchmark.csproj

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@
1616
<ItemGroup>
1717
<PackageReference Include="BenchmarkDotNet" Version="0.15.2" />
1818
<PackageReference Include="JsonCons.JsonPath" Version="1.1.0" />
19-
<PackageReference Include="JsonCraft.JsonPath" Version="1.0.0" />
2019
<PackageReference Include="JsonPatch.Net" Version="3.3.0" />
2120
<PackageReference Include="JsonPath.Net" Version="2.1.1" />
2221
<PackageReference Include="Microsoft.AspNetCore.JsonPatch" Version="9.0.6" />

test/Hyperbee.Json.Benchmark/JsonPathParseAndSelectEvaluator.cs renamed to test/Hyperbee.Json.Benchmark/JsonPathBenchmark.cs

Lines changed: 57 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
using System.Text.Json;
1+
using System.Text.Json;
22
using System.Text.Json.Nodes;
33
using BenchmarkDotNet.Attributes;
44
using BenchmarkDotNet.Engines;
@@ -10,34 +10,60 @@
1010
namespace Hyperbee.Json.Benchmark;
1111

1212

13-
public class JsonPathParseAndSelectEvaluator
13+
public class JsonPathBenchmark
1414
{
15+
/*
1516
[Params(
16-
"$.store.book[0].title",
17-
"$.store.book[*].author",
18-
"$.store.book[?(@.price < 10)].title",
19-
"$.store.bicycle.color",
20-
"$.store.book[*]",
21-
"$.store..price",
22-
"$..author",
23-
"$.store.book[?(@.price > 10 && @.price < 20)]",
24-
"$.store.book[?(@.category == 'fiction')]",
25-
"$.store.book[-1:]",
26-
"$.store.book[:2]",
27-
"$..book[0,1]",
28-
"$..*",
29-
"$..['bicycle','price']",
30-
"$..[?(@.price < 10)]",
31-
"$.store.book[?(@.author && @.title)]",
17+
| | `$..* First()`
18+
| `$..*`
19+
| `$..price`
20+
| `$.store.book[?(@.price == 8.99)]`
21+
| `$.store.book[0]`
22+
)]
23+
*/
24+
25+
[Params(
26+
// Root and Wildcard
27+
"$",
3228
"$.store.*",
33-
"$",
34-
"$.store.book[0]",
35-
"$..book[0]",
36-
"$.store.book[0,1]",
37-
"$.store.book['category','author']",
38-
"$..book[[email protected]]",
39-
"$.store.book[[email protected] == 8.99]",
40-
"$..book[[email protected] == 8.99 && @.category == 'fiction']"
29+
"$.store.* #First()", // Test Enumerable.First()
30+
31+
// Property and Index Access
32+
"$.store.book[0]",
33+
"$.store.book[0].title",
34+
"$.store.book[*]",
35+
"$.store.book[*].author",
36+
"$.store.book['category','author']",
37+
38+
// Recursive Descent
39+
"$.store..price",
40+
"$..author",
41+
"$..*",
42+
"$..['bicycle','price']",
43+
"$..book[0,1]",
44+
"$..book[[email protected]]",
45+
46+
// Filters
47+
"$.store.book[?(@.price < 10)].title",
48+
"$.store.book[?(@.price > 10 && @.price < 20)]",
49+
"$.store.book[?(@.category == 'fiction')]",
50+
"$.store.book[?(@.author && @.title)]",
51+
"$.store.book[?(@.price == 8.99)]",
52+
"$..[?(@.price < 10)]",
53+
"$..book[[email protected] == 8.99 && @.category == 'fiction']",
54+
"$.store.book[?(@.price < 10 || @.category == 'fiction')]",
55+
"$.store.book[?([email protected])]",
56+
"$.store.book[?(length(@.title) > 10)]",
57+
58+
59+
// Array Slices and Unions
60+
"$.store.book[-1:]",
61+
"$.store.book[0,1]",
62+
"$.store.book[:2]",
63+
"$.store.book[0:3:2]",
64+
65+
// Property Access (Direct)
66+
"$.store.bicycle.color"
4167
)]
4268
public string Filter;
4369

@@ -100,7 +126,7 @@ public void Setup()
100126

101127
public (string, bool) GetFilter()
102128
{
103-
const string First = " ::First()";
129+
const string First = " #First()";
104130

105131
return Filter.EndsWith( First )
106132
? (Filter[..^First.Length], true)
@@ -126,7 +152,7 @@ public void Hyperbee_JsonElement()
126152
Consume( select, first );
127153
}
128154

129-
[Benchmark( Description = "Hyperbee.JsonNode" )]
155+
//[Benchmark( Description = "Hyperbee.JsonNode" )]
130156
public void Hyperbee_JsonNode()
131157
{
132158
var (filter, first) = GetFilter();
@@ -137,7 +163,7 @@ public void Hyperbee_JsonNode()
137163
Consume( select, first );
138164
}
139165

140-
[Benchmark( Description = "Newtonsoft.JObject" )]
166+
//[Benchmark( Description = "Newtonsoft.JObject" )]
141167
public void Newtonsoft_JObject()
142168
{
143169
var (filter, first) = GetFilter();
@@ -148,7 +174,7 @@ public void Newtonsoft_JObject()
148174
Consume( select, first );
149175
}
150176

151-
[Benchmark( Description = "JsonEverything.JsonNode" )]
177+
//[Benchmark( Description = "JsonEverything.JsonNode" )]
152178
public void JsonEverything_JsonNode()
153179
{
154180
var (filter, first) = GetFilter();
@@ -160,7 +186,7 @@ public void JsonEverything_JsonNode()
160186
Consume( select, first );
161187
}
162188

163-
[Benchmark( Description = "JsonCons.JsonElement" )]
189+
//[Benchmark( Description = "JsonCons.JsonElement" )]
164190
public void JsonCons_JsonElement()
165191
{
166192
var (filter, first) = GetFilter();
@@ -171,15 +197,4 @@ public void JsonCons_JsonElement()
171197

172198
Consume( select, first );
173199
}
174-
175-
[Benchmark( Description = "JsonCraft.JsonElement" )]
176-
public void JsonCraft_JsonElement()
177-
{
178-
var (filter, first) = GetFilter();
179-
180-
var element = JsonDocument.Parse( Document ).RootElement;
181-
var select = JsonCraft.JsonPath.JsonExtensions.SelectElements( element, filter );
182-
183-
Consume( select, first );
184-
}
185200
}

0 commit comments

Comments
 (0)