From 1b7bc7f34703cf71f1c48f45444183c0c38e7b65 Mon Sep 17 00:00:00 2001 From: f3ath Date: Mon, 23 Sep 2024 20:34:26 -0700 Subject: [PATCH 1/2] Cleanup --- CHANGELOG.md | 5 + benchmark/parsing.dart | 23 +++ lib/src/fun/fun_factory.dart | 20 +-- lib/src/grammar/json_path.dart | 137 ++++++++---------- .../grammar/singular_segment_sequence.dart | 23 +++ lib/src/grammar/strings.dart | 4 +- lib/src/json_path_parser.dart | 2 +- lib/src/node_match.dart | 8 +- lib/src/normalized/name_selector.dart | 2 +- pubspec.yaml | 4 +- 10 files changed, 125 insertions(+), 103 deletions(-) create mode 100644 benchmark/parsing.dart create mode 100644 lib/src/grammar/singular_segment_sequence.dart diff --git a/CHANGELOG.md b/CHANGELOG.md index 5301b6d..914d5a0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,10 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [Unreleased] +### Changed +- Minor performance improvements + ## [0.7.4] - 2024-08-03 ### Changed - CTS updated @@ -205,6 +209,7 @@ Previously, no modification would be made and no errors/exceptions thrown. ### Added - Basic design draft +[Unreleased]: https://github.com/f3ath/jessie/compare/0.7.4...HEAD [0.7.4]: https://github.com/f3ath/jessie/compare/0.7.3...0.7.4 [0.7.3]: https://github.com/f3ath/jessie/compare/0.7.2...0.7.3 [0.7.2]: https://github.com/f3ath/jessie/compare/0.7.1...0.7.2 diff --git a/benchmark/parsing.dart b/benchmark/parsing.dart new file mode 100644 index 0000000..ef3431c --- /dev/null +++ b/benchmark/parsing.dart @@ -0,0 +1,23 @@ +import 'package:json_path/json_path.dart'; + +void main() { + const e1 = + r"$..store[?(@.type == 'electronics')].items[?(@.price > 100 && @.availability == 'in stock')].details[?(@.specs.screen.size >= 40 && search(@.specs.processor, 'Intel|AMD'))].reviews[*].comments[?(@.rating >= 4 && @.verified == true)].content[?(length(@) > 100)]"; + const e2 = + r"$..book[0:10][?(match(@.author, '(J.K.|George R.R.) Martin') && @.price < 30 && @.published >= '2010-01-01')].summary[?(length(@) > 200)]"; + const e3 = + r"$..store.bicycle[?(@.price > 50)].model[?match(@, '(Mountain|Road)')]"; + const e4 = + r"$..orders[*][?(@.status == 'delivered' && value(@.items[0:5][?(@.price > 50 && match(@.category, '(Electronics|Books)'))].quantity) > 1)].tracking[*].history[?(@.location == 'Warehouse' && @.status == 'out for delivery')]"; + + final start = DateTime.now(); + for (var i = 0; i < 50000; i++) { + JsonPath(e1); + JsonPath(e2); + JsonPath(e3); + JsonPath(e4); + } + final end = DateTime.now(); + print( + 'Duration: ${end.difference(start).inMilliseconds} ms'); +} diff --git a/lib/src/fun/fun_factory.dart b/lib/src/fun/fun_factory.dart index 5f78fea..e41b334 100644 --- a/lib/src/fun/fun_factory.dart +++ b/lib/src/fun/fun_factory.dart @@ -32,17 +32,11 @@ class FunFactory { Expression nodes(FunCall call) => _any(call); /// Returns a function to use as an argument for another function. - Expression _any(FunCall call) { - final name = call.name; - final args = call.args; - try { - if (args.length == 1) return _any1(name, args[0]); - if (args.length == 2) return _any2(name, args[0], args[1]); - } on StateError catch (e) { - throw FormatException(e.message); - } - throw Exception('Type mismatch'); - } + Expression _any(FunCall call) => switch (call.args) { + [var a] => _any1(call.name, a), + [var a, var b] => _any2(call.name, a, b), + _ => throw Exception('Invalid number of args for ${call.name}()'), + }; Expression _any1(String name, Expression a0) { final f = _getFun1(name); @@ -79,13 +73,13 @@ class FunFactory { Fun1 _getFun1(String name) { final f = _fun1[name]; if (f is Fun1) return f; - throw StateError('Function "$name" of 1 argument is not found'); + throw FormatException('Function "$name" of 1 argument is not found'); } Fun2 _getFun2(String name) { final f = _fun2[name]; if (f is Fun2) return f; - throw StateError('Function "$name" of 2 arguments is not found'); + throw FormatException('Function "$name" of 2 arguments is not found'); } static Expression cast(Expression arg, diff --git a/lib/src/grammar/json_path.dart b/lib/src/grammar/json_path.dart index e050331..0e449c0 100644 --- a/lib/src/grammar/json_path.dart +++ b/lib/src/grammar/json_path.dart @@ -14,6 +14,7 @@ import 'package:json_path/src/grammar/negatable.dart'; import 'package:json_path/src/grammar/parser_ext.dart'; import 'package:json_path/src/grammar/select_all_recursively.dart'; import 'package:json_path/src/grammar/sequence_selector.dart'; +import 'package:json_path/src/grammar/singular_segment_sequence.dart'; import 'package:json_path/src/grammar/strings.dart'; import 'package:json_path/src/grammar/union_selector.dart'; import 'package:json_path/src/grammar/wildcard.dart'; @@ -28,27 +29,25 @@ class JsonPathGrammarDefinition final FunFactory _fun; @override - Parser> start() => ref0(_absPath).end(); + Parser> start() => _absPath().end(); - Parser _unionElement() => [ - arraySlice, - arrayIndex, - wildcard, - quotedString.map(childSelector), - _expressionFilter() - ].toChoiceParser().trim(); + Parser> _absPath() => _segmentSequence() + .skip(before: char(r'$')) + .map((expr) => Expression((node) => expr.call(node.root))); - Parser _singularUnionElement() => [ - arrayIndex, - quotedString.map(childSelector), + Parser> _segmentSequence() => + _segment().star().map(sequenceSelector).map(Expression.new); + + Parser _segment() => [ + dotName, + wildcard.skip(before: char('.')), + ref0(_union), + ref0(_recursion), ].toChoiceParser().trim(); Parser _union() => _unionElement().toList().inBrackets().map(unionSelector); - Parser _singularUnion() => - _singularUnionElement().inBrackets(); - Parser _recursion() => [ wildcard, _union(), @@ -58,38 +57,18 @@ class JsonPathGrammarDefinition .skip(before: string('..')) .map((value) => sequenceSelector([selectAllRecursively, value])); - Parser> _parenExpr() => negatable(_logicalExpr().inParens()); - - Parser _funArgument() => [ - literal, - ref0(_singularFilterPath), - ref0(_filterPath), - ref0(_valueFunExpr), - ref0(_logicalFunExpr), - ref0(_nodesFunExpr), - ref0(_logicalExpr), + Parser _unionElement() => [ + arraySlice, + arrayIndex, + wildcard, + quotedString.map(childSelector), + _expressionFilter() ].toChoiceParser().trim(); - Parser _funCall(T Function(FunCall) toFun) => - (funName & _funArgument().toList().inParens()) - .map((v) => FunCall(v[0], v[1])) - .tryMap(toFun); - - Parser> _valueFunExpr() => _funCall(_fun.value); - - Parser> _nodesFunExpr() => _funCall(_fun.nodes); - - Parser> _logicalFunExpr() => _funCall(_fun.logical); - - Parser> _comparable() => [ - literal, - _singularFilterPath().map((expr) => expr.map((v) => v.asValue)), - _valueFunExpr(), - ].toChoiceParser(); - - Parser> _logicalExpr() => _logicalOrExpr(); + Parser _expressionFilter() => + _logicalExpr().skip(before: string('?').trim()).map(filterSelector); - Parser> _logicalOrExpr() => _logicalOrSequence() + Parser> _logicalExpr() => _logicalOrSequence() .map((list) => list.reduce((a, b) => a.merge(b, (a, b) => a || b))); Parser>> _logicalOrSequence() => @@ -109,54 +88,55 @@ class JsonPathGrammarDefinition failureJoiner: (a, b) => Failure(a.buffer, a.position, 'Expression expected')); - Parser> _filterPath() => [ - ref0(_relPath), - ref0(_absPath), - ].toChoiceParser(); - - Parser> _singularFilterPath() => [ - ref0(_singularRelPath), - ref0(_singularAbsPath), - ].toChoiceParser(); - - Parser> _existenceTest() => - ref0(_filterPath).map((value) => value.map((v) => v.asLogical)); + Parser> _parenExpr() => negatable(_logicalExpr().inParens()); Parser> _testExpr() => negatable([ _existenceTest(), _logicalFunExpr(), ].toChoiceParser()); - Parser _expressionFilter() => - _logicalExpr().skip(before: string('?').trim()).map(filterSelector); + Parser> _existenceTest() => + _filterPath().map((value) => value.map((v) => v.asLogical)); - Parser _segment() => [ - dotName, - wildcard.skip(before: char('.')), - ref0(_union), - ref0(_recursion), - ].toChoiceParser().trim(); + Parser> _logicalFunExpr() => _funCall(_fun.logical); - Parser _singularSegment() => [ - dotName, - ref0(_singularUnion), + Parser _funCall(T Function(FunCall) toFun) => + (funName & _funArgument().toList().inParens()) + .map((v) => FunCall(v[0], v[1])) + .tryMap(toFun); + + Parser _funArgument() => [ + literal, + _singularFilterPath(), + _filterPath(), + ref0(_valueFunExpr), + ref0(_logicalFunExpr), + ref0(_nodesFunExpr), + ref0(_logicalExpr), ].toChoiceParser().trim(); - Parser> _segmentSequence() => - _segment().star().map(sequenceSelector).map(Expression.new); + Parser> _singularFilterPath() => [ + ref0(_singularRelPath), + ref0(_singularAbsPath), + ].toChoiceParser(); - Parser> _singularSegmentSequence() => - _singularSegment() - .star() - .map(singularSequenceSelector) - .map(Expression.new); + Parser> _valueFunExpr() => _funCall(_fun.value); - Parser> _absPath() => _segmentSequence() - .skip(before: char(r'$')) - .map((expr) => Expression((node) => expr.call(node.root))); + Parser> _nodesFunExpr() => _funCall(_fun.nodes); + + Parser> _comparable() => [ + literal, + _singularFilterPath().map((expr) => expr.map((v) => v.asValue)), + _valueFunExpr(), + ].toChoiceParser(); + + Parser> _filterPath() => [ + ref0(_relPath), + ref0(_absPath), + ].toChoiceParser(); Parser> _singularAbsPath() => - _singularSegmentSequence() + singularSegmentSequence .skip(before: char(r'$'), after: _segment().not()) .map((expr) => Expression((node) => expr.call(node.root))); @@ -164,6 +144,5 @@ class JsonPathGrammarDefinition _segmentSequence().skip(before: char('@')); Parser> _singularRelPath() => - _singularSegmentSequence() - .skip(before: char('@'), after: _segment().not()); + singularSegmentSequence.skip(before: char('@'), after: _segment().not()); } diff --git a/lib/src/grammar/singular_segment_sequence.dart b/lib/src/grammar/singular_segment_sequence.dart new file mode 100644 index 0000000..25cf5f8 --- /dev/null +++ b/lib/src/grammar/singular_segment_sequence.dart @@ -0,0 +1,23 @@ +import 'package:json_path/src/expression/expression.dart'; +import 'package:json_path/src/grammar/array_index.dart'; +import 'package:json_path/src/grammar/child_selector.dart'; +import 'package:json_path/src/grammar/dot_name.dart'; +import 'package:json_path/src/grammar/parser_ext.dart'; +import 'package:json_path/src/grammar/sequence_selector.dart'; +import 'package:json_path/src/grammar/strings.dart'; +import 'package:petitparser/petitparser.dart'; + +final _singularUnionElement = [ + arrayIndex, + quotedString.map(childSelector), +].toChoiceParser().trim(); + +final _singularUnion = _singularUnionElement.inBrackets(); + +final _singularSegment = [ + dotName, + _singularUnion, +].toChoiceParser().trim(); + +final singularSegmentSequence = + _singularSegment.star().map(singularSequenceSelector).map(Expression.new); diff --git a/lib/src/grammar/strings.dart b/lib/src/grammar/strings.dart index cd4ee2b..289b3bf 100644 --- a/lib/src/grammar/strings.dart +++ b/lib/src/grammar/strings.dart @@ -26,9 +26,7 @@ final _escapedControl = [ _escapedTab ].toChoiceParser(); -// The parser does not seem to support Unicode 6.0 boundary (0x10FFFF). -// We're limiting ourselves to Unicode 1.0 boundary (0xFFFF). -// TODO: work around by using surrogate pairs +// The highest unicode character final _unicodeBoundary = String.fromCharCode(0xFFFF); // Exclude double quote '"' and back slash '\' diff --git a/lib/src/json_path_parser.dart b/lib/src/json_path_parser.dart index 4aa9585..7eebfb8 100644 --- a/lib/src/json_path_parser.dart +++ b/lib/src/json_path_parser.dart @@ -20,7 +20,7 @@ class JsonPathParser { JsonPathParser._(Iterable functions) : _parser = - JsonPathGrammarDefinition(FunFactory([..._stdFun, ...functions])) + JsonPathGrammarDefinition(FunFactory(_stdFun.followedBy(functions))) .build>(); /// The standard instance is pre-cached to speed up parsing when only diff --git a/lib/src/node_match.dart b/lib/src/node_match.dart index dee6c2a..876ee11 100644 --- a/lib/src/node_match.dart +++ b/lib/src/node_match.dart @@ -20,7 +20,7 @@ class NodeMatch implements JsonPathMatch { final Object? value; } -extension _NodeExt on Node { +extension on Node { Iterable trace() sync* { if (key != null) { yield* parent!.trace(); @@ -35,7 +35,7 @@ extension _NodeExt on Node { JsonPointer pointer() => JsonPointer.build(trace().map((e) => e.toString())); String path() => r'$' + trace().map(_segment).join(); -} -Object _segment(Object? v) => - v is int ? IndexSelector(v) : NameSelector(v.toString()); + Object _segment(Object? v) => + v is int ? IndexSelector(v) : NameSelector(v.toString()); +} diff --git a/lib/src/normalized/name_selector.dart b/lib/src/normalized/name_selector.dart index 80fa334..018eb38 100644 --- a/lib/src/normalized/name_selector.dart +++ b/lib/src/normalized/name_selector.dart @@ -8,7 +8,7 @@ class NameSelector { String toString() => "['${name.escaped}']"; } -extension _StringExt on String { +extension on String { /// Returns a string with all characters escaped as unicode entities. String get unicodeEscaped => codeUnits.map((c) => '\\u${c.toRadixString(16).padLeft(4, '0')}').join(); diff --git a/pubspec.yaml b/pubspec.yaml index 614ce0d..d22cd82 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -4,7 +4,7 @@ description: "Implementation of RFC 9535 - JSONPath: Query Expressions for JSON. homepage: "https://github.com/f3ath/jessie" environment: - sdk: '>=3.0.0 <4.0.0' + sdk: '>=3.5.0 <4.0.0' dependencies: iregexp: ^0.1.2 @@ -14,7 +14,7 @@ dependencies: dev_dependencies: check_coverage: ^0.0.4 - lints: ">=3.0.0 <5.0.0" + lints: ^4.0.0 path: ^1.8.2 test: ^1.21.1 From de15960fc63d2e027af9f29dd7f04b2598acda0b Mon Sep 17 00:00:00 2001 From: f3ath Date: Mon, 23 Sep 2024 20:35:53 -0700 Subject: [PATCH 2/2] Cleanup --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index d22cd82..be6c41e 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -4,7 +4,7 @@ description: "Implementation of RFC 9535 - JSONPath: Query Expressions for JSON. homepage: "https://github.com/f3ath/jessie" environment: - sdk: '>=3.5.0 <4.0.0' + sdk: '>=3.0.0 <4.0.0' dependencies: iregexp: ^0.1.2