diff --git a/CHANGELOG.md b/CHANGELOG.md index e58222b..d0e8d99 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,11 @@ 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). +## [0.7.0] - 2023-12-29 +### Changed +- Renamed `Nodes` to `NodeList` +- Bumped the CTS to the latest version + ## [0.6.6] - 2023-09-23 ### Fixed - Logical expressions should be allowed in function arguments @@ -176,6 +181,7 @@ Previously, no modification would be made and no errors/exceptions thrown. ### Added - Basic design draft +[0.7.0]: https://github.com/f3ath/jessie/compare/0.6.6...0.7.0 [0.6.6]: https://github.com/f3ath/jessie/compare/0.6.5...0.6.6 [0.6.5]: https://github.com/f3ath/jessie/compare/0.6.4...0.6.5 [0.6.4]: https://github.com/f3ath/jessie/compare/0.6.3...0.6.4 diff --git a/lib/src/expression/nodes.dart b/lib/src/expression/nodes.dart index 8e42243..45c3059 100644 --- a/lib/src/expression/nodes.dart +++ b/lib/src/expression/nodes.dart @@ -1,9 +1,21 @@ import 'package:json_path/src/node.dart'; import 'package:maybe_just_nothing/maybe_just_nothing.dart'; -typedef Nodes = Iterable; +typedef NodeList = Iterable; -extension NodesExt on Nodes { +class SingularNodeList with NodeList { + SingularNodeList(this._nodes); + + SingularNodeList.from(Node? node) + : this(node != null ? [node] : const Iterable.empty()); + + final NodeList _nodes; + + @override + Iterator> get iterator => _nodes.iterator; +} + +extension NodeListExt on NodeList { Maybe get asValue => length == 1 ? Just(single.value) : const Nothing(); bool get asLogical => isNotEmpty; diff --git a/lib/src/fun/extra/siblings.dart b/lib/src/fun/extra/siblings.dart index a0ebe53..708ad5e 100644 --- a/lib/src/fun/extra/siblings.dart +++ b/lib/src/fun/extra/siblings.dart @@ -1,13 +1,13 @@ import 'package:json_path/fun_sdk.dart'; /// Returns all siblings of the given nodes. -class Siblings implements Fun1 { +class Siblings implements Fun1 { const Siblings(); @override final name = 'siblings'; @override - Nodes call(Nodes nodes) => nodes + NodeList call(NodeList nodes) => nodes .expand((node) => node.parent?.children.where((it) => node != it) ?? []); } diff --git a/lib/src/fun/fun.dart b/lib/src/fun/fun.dart index c50861e..11d3ff6 100644 --- a/lib/src/fun/fun.dart +++ b/lib/src/fun/fun.dart @@ -8,7 +8,7 @@ abstract interface class Fun { /// The return type [R] and the argument type [T] must be one of the following: /// - [bool] /// - [Maybe] -/// - [Nodes] +/// - [NodeList] abstract interface class Fun1 extends Fun { /// Applies the given arguments. /// This method MUST throw an [Exception] on invalid args. @@ -19,7 +19,7 @@ abstract interface class Fun1 extends Fun { /// The return type [R] and the argument types [T1], [T2] must be one of the following: /// - [bool] /// - [Maybe] -/// - [Nodes] +/// - [NodeList] abstract interface class Fun2 extends Fun { /// Applies the given arguments. diff --git a/lib/src/fun/fun_factory.dart b/lib/src/fun/fun_factory.dart index e7c14c0..dbbf380 100644 --- a/lib/src/fun/fun_factory.dart +++ b/lib/src/fun/fun_factory.dart @@ -22,67 +22,78 @@ class FunFactory { final _fun1 = {}; final _fun2 = {}; - /// Returns a function to use in comparable context. - Expression comparable(FunCall call) => any(call); + /// Returns a value-type function to use in comparable context. + Expression value(FunCall call) => _any(call); - /// Returns a function to use in logical context. - Expression logical(FunCall call) => any(call); + /// Returns a logical-type function to use in logical context. + Expression logical(FunCall call) => _any(call); + + /// Returns a nodes-type function. + Expression nodes(FunCall call) => _any(call); /// Returns a function to use as an argument for another function. - Expression any(FunCall call) { + 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 TypeError catch (e) { - throw FormatException('Invalid argument: $e'); + 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 any1(String name, Expression a0) { - final f = _get1(name); - final cast0 = cast(value: f is Fun1, logical: f is Fun1); - return a0.map(cast0).map(f.call); + Expression _any1(String name, Expression a0) { + final f = _getFun1(name); + final cast0 = cast(a0, + value: f is Fun1, + logical: f is Fun1, + nodes: f is Fun1); + return cast0.map(f.call); } - Expression any2( + Expression _any2( String name, Expression a0, Expression a1) { - final f = _get2(name); + final f = _getFun2(name); final cast0 = cast( - value: f is Fun2, - logical: f is Fun2); + a0, + value: f is Fun2, + logical: f is Fun2, + nodes: f is Fun2, + ); final cast1 = cast( - value: f is Fun2, - logical: f is Fun2); - return a0.map(cast0).merge(a1.map(cast1), f.call); + a1, + value: f is Fun2, + logical: f is Fun2, + nodes: f is Fun2, + ); + return cast0.merge(cast1, f.call); } - Fun1 _get1(String name) { + Fun1 _getFun1(String name) { final f = _fun1[name]; if (f is Fun1) return f; throw StateError('Function "$name" of 1 argument is not found'); } - Fun2 _get2(String name) { + Fun2 _getFun2(String name) { final f = _fun2[name]; if (f is Fun2) return f; throw StateError('Function "$name" of 2 arguments is not found'); } - static Object Function(Object) cast( - {required bool value, required bool logical}) { - if (value) return _value; - if (logical) return _logical; - return _nodes; + static Expression cast(Expression arg, + {required bool value, required bool logical, required bool nodes}) { + if (value) { + if (arg is Expression) return arg; + if (arg is Expression) return arg.map((v) => v.asValue); + } + if (logical) { + if (arg is Expression) return arg; + if (arg is Expression) return arg.map((v) => v.asLogical); + } + if (nodes && arg is Expression) return arg; + throw Exception('Arg type mismatch'); } - - static Maybe _value(v) => (v is Maybe) ? v : _nodes(v).asValue; - - static bool _logical(v) => (v is bool) ? v : _nodes(v).asLogical; - - static Nodes _nodes(v) => v as Nodes; } diff --git a/lib/src/fun/fun_validator.dart b/lib/src/fun/fun_validator.dart index 2082616..ad95062 100644 --- a/lib/src/fun/fun_validator.dart +++ b/lib/src/fun/fun_validator.dart @@ -14,28 +14,28 @@ class FunValidator { if (f is Fun1) { if (f is! Fun1 && f is! Fun1 && - f is! Fun1) { + f is! Fun1) { yield 'Invalid return type in function ${f.name}'; } if (f is! Fun1 && - f is! Fun1 && + f is! Fun1 && f is! Fun1) { yield 'Invalid type of the first argument in function ${f.name}'; } } else if (f is Fun2) { if (f is! Fun2 && f is! Fun2 && - f is! Fun2) { + f is! Fun2) { yield 'Invalid return type in function ${f.name}'; } if (f is! Fun2 && f is! Fun2 && - f is! Fun2) { + f is! Fun2) { yield 'Invalid type of the first argument in function ${f.name}'; } if (f is! Fun2 && f is! Fun2 && - f is! Fun2) { + f is! Fun2) { yield 'Invalid type of the second argument in function ${f.name}'; } } else { diff --git a/lib/src/fun/standard/count.dart b/lib/src/fun/standard/count.dart index 63f3414..ded104d 100644 --- a/lib/src/fun/standard/count.dart +++ b/lib/src/fun/standard/count.dart @@ -4,12 +4,12 @@ import 'package:maybe_just_nothing/maybe_just_nothing.dart'; /// The standard `count()` function which returns the number of nodes in a node list. /// See https://ietf-wg-jsonpath.github.io/draft-ietf-jsonpath-base/draft-ietf-jsonpath-base.html#name-count-function-extension -class Count implements Fun1 { +class Count implements Fun1 { const Count(); @override final name = 'count'; @override - Maybe call(Nodes nodes) => Just(nodes.length); + Maybe call(NodeList nodes) => Just(nodes.length); } diff --git a/lib/src/fun/standard/value.dart b/lib/src/fun/standard/value.dart index 7664952..e0ada03 100644 --- a/lib/src/fun/standard/value.dart +++ b/lib/src/fun/standard/value.dart @@ -2,12 +2,12 @@ import 'package:json_path/fun_sdk.dart'; /// The standard `value()` function. /// See https://ietf-wg-jsonpath.github.io/draft-ietf-jsonpath-base/draft-ietf-jsonpath-base.html#name-value-function-extension -class Value implements Fun1 { +class Value implements Fun1 { const Value(); @override final name = 'value'; @override - Maybe call(Nodes arg) => arg.asValue; + Maybe call(NodeList arg) => arg.asValue; } diff --git a/lib/src/grammar/array_index_selector.dart b/lib/src/grammar/array_index_selector.dart index c59a19d..9757647 100644 --- a/lib/src/grammar/array_index_selector.dart +++ b/lib/src/grammar/array_index_selector.dart @@ -1,6 +1,5 @@ +import 'package:json_path/fun_sdk.dart'; import 'package:json_path/src/selector.dart'; -Selector arrayIndexSelector(int offset) => (node) sync* { - final element = node.element(offset); - if (element != null) yield element; - }; +SingularSelector arrayIndexSelector(int offset) => + (node) => SingularNodeList.from(node.element(offset)); diff --git a/lib/src/grammar/child_selector.dart b/lib/src/grammar/child_selector.dart index 7e5130a..e7e75b6 100644 --- a/lib/src/grammar/child_selector.dart +++ b/lib/src/grammar/child_selector.dart @@ -1,6 +1,5 @@ +import 'package:json_path/fun_sdk.dart'; import 'package:json_path/src/selector.dart'; -Selector childSelector(String key) => (node) sync* { - final child = node.child(key); - if (child != null) yield child; - }; +SingularSelector childSelector(String key) => + (node) => SingularNodeList.from(node.child(key)); diff --git a/lib/src/grammar/json_path.dart b/lib/src/grammar/json_path.dart index ad69104..e050331 100644 --- a/lib/src/grammar/json_path.dart +++ b/lib/src/grammar/json_path.dart @@ -21,13 +21,14 @@ import 'package:json_path/src/selector.dart'; import 'package:maybe_just_nothing/maybe_just_nothing.dart'; import 'package:petitparser/petitparser.dart'; -class JsonPathGrammarDefinition extends GrammarDefinition> { +class JsonPathGrammarDefinition + extends GrammarDefinition> { JsonPathGrammarDefinition(this._fun); final FunFactory _fun; @override - Parser> start() => ref0(_absPath).end(); + Parser> start() => ref0(_absPath).end(); Parser _unionElement() => [ arraySlice, @@ -37,7 +38,7 @@ class JsonPathGrammarDefinition extends GrammarDefinition> { _expressionFilter() ].toChoiceParser().trim(); - Parser _singularUnionElement() => [ + Parser _singularUnionElement() => [ arrayIndex, quotedString.map(childSelector), ].toChoiceParser().trim(); @@ -45,8 +46,8 @@ class JsonPathGrammarDefinition extends GrammarDefinition> { Parser _union() => _unionElement().toList().inBrackets().map(unionSelector); - Parser _singularUnion() => - _singularUnionElement().toSingularList().inBrackets().map(unionSelector); + Parser _singularUnion() => + _singularUnionElement().inBrackets(); Parser _recursion() => [ wildcard, @@ -61,8 +62,11 @@ class JsonPathGrammarDefinition extends GrammarDefinition> { Parser _funArgument() => [ literal, - _filterPath(), - ref0(_funExpr), + ref0(_singularFilterPath), + ref0(_filterPath), + ref0(_valueFunExpr), + ref0(_logicalFunExpr), + ref0(_nodesFunExpr), ref0(_logicalExpr), ].toChoiceParser().trim(); @@ -71,16 +75,16 @@ class JsonPathGrammarDefinition extends GrammarDefinition> { .map((v) => FunCall(v[0], v[1])) .tryMap(toFun); - Parser _funExpr() => _funCall(_fun.any); + Parser> _valueFunExpr() => _funCall(_fun.value); - Parser> _comparableFunExpr() => _funCall(_fun.comparable); + Parser> _nodesFunExpr() => _funCall(_fun.nodes); Parser> _logicalFunExpr() => _funCall(_fun.logical); Parser> _comparable() => [ literal, _singularFilterPath().map((expr) => expr.map((v) => v.asValue)), - _comparableFunExpr(), + _valueFunExpr(), ].toChoiceParser(); Parser> _logicalExpr() => _logicalOrExpr(); @@ -105,12 +109,12 @@ class JsonPathGrammarDefinition extends GrammarDefinition> { failureJoiner: (a, b) => Failure(a.buffer, a.position, 'Expression expected')); - Parser> _filterPath() => [ + Parser> _filterPath() => [ ref0(_relPath), ref0(_absPath), ].toChoiceParser(); - Parser> _singularFilterPath() => [ + Parser> _singularFilterPath() => [ ref0(_singularRelPath), ref0(_singularAbsPath), ].toChoiceParser(); @@ -133,28 +137,33 @@ class JsonPathGrammarDefinition extends GrammarDefinition> { ref0(_recursion), ].toChoiceParser().trim(); - Parser _singularSegment() => [ + Parser _singularSegment() => [ dotName, ref0(_singularUnion), ].toChoiceParser().trim(); - Parser> _segmentSequence() => + Parser> _segmentSequence() => _segment().star().map(sequenceSelector).map(Expression.new); - Parser> _singularSegmentSequence() => - _singularSegment().star().map(sequenceSelector).map(Expression.new); + Parser> _singularSegmentSequence() => + _singularSegment() + .star() + .map(singularSequenceSelector) + .map(Expression.new); - Parser> _absPath() => _segmentSequence() + Parser> _absPath() => _segmentSequence() .skip(before: char(r'$')) .map((expr) => Expression((node) => expr.call(node.root))); - Parser> _singularAbsPath() => _singularSegmentSequence() - .skip(before: char(r'$')) - .map((expr) => Expression((node) => expr.call(node.root))); + Parser> _singularAbsPath() => + _singularSegmentSequence() + .skip(before: char(r'$'), after: _segment().not()) + .map((expr) => Expression((node) => expr.call(node.root))); - Parser> _relPath() => + Parser> _relPath() => _segmentSequence().skip(before: char('@')); - Parser> _singularRelPath() => - _singularSegmentSequence().skip(before: char('@')); + Parser> _singularRelPath() => + _singularSegmentSequence() + .skip(before: char('@'), after: _segment().not()); } diff --git a/lib/src/grammar/parser_ext.dart b/lib/src/grammar/parser_ext.dart index 6b358d2..56e0716 100644 --- a/lib/src/grammar/parser_ext.dart +++ b/lib/src/grammar/parser_ext.dart @@ -10,9 +10,6 @@ extension ParserExt on Parser { skip(before: (separator ?? char(',')).trim()).star() ].toSequenceParser().map>((v) => v.expand((e) => e).toList()); - /// Makes a list consisting of a single element. - Parser> toSingularList() => map((v) => [v]); - /// Same in parenthesis. Parser inParens() => skip(before: char('('), after: char(')')); diff --git a/lib/src/grammar/sequence_selector.dart b/lib/src/grammar/sequence_selector.dart index b6144d9..bf30687 100644 --- a/lib/src/grammar/sequence_selector.dart +++ b/lib/src/grammar/sequence_selector.dart @@ -1,10 +1,19 @@ -import 'package:json_path/src/node.dart'; +import 'package:json_path/src/expression/nodes.dart'; import 'package:json_path/src/selector.dart'; -Selector sequenceSelector(Iterable selectors) { - final filter = selectors.fold<_Filter>((v) => v, - (filter, selector) => (nodes) => filter(nodes).expand(selector)); - return (node) => filter([node]); -} +Selector sequenceSelector(Iterable selectors) => + (node) => selectors.fold<_Filter>( + (v) => v, + (filter, selector) => + (nodes) => filter(nodes).expand(selector))([node]); -typedef _Filter = Iterable Function(Iterable nodes); +SingularSelector singularSequenceSelector( + Iterable selectors) => + (node) => selectors.fold<_SingularFilter>( + SingularNodeList.new, + (filter, selector) => (nodes) => + SingularNodeList(filter(nodes).expand(selector)))([node]); + +typedef _Filter = NodeList Function(NodeList nodes); + +typedef _SingularFilter = SingularNodeList Function(NodeList nodes); diff --git a/lib/src/json_path_parser.dart b/lib/src/json_path_parser.dart index 34b7a51..4aa9585 100644 --- a/lib/src/json_path_parser.dart +++ b/lib/src/json_path_parser.dart @@ -21,7 +21,7 @@ class JsonPathParser { JsonPathParser._(Iterable functions) : _parser = JsonPathGrammarDefinition(FunFactory([..._stdFun, ...functions])) - .build>(); + .build>(); /// The standard instance is pre-cached to speed up parsing when only /// the standard built-in functions are used. @@ -36,7 +36,7 @@ class JsonPathParser { Value(), ]; - final Parser> _parser; + final Parser> _parser; /// Parses the JSONPath from s string [expression]. /// Returns an instance of [JsonPath] or throws a [FormatException]. diff --git a/lib/src/selector.dart b/lib/src/selector.dart index cd3948e..3b23aff 100644 --- a/lib/src/selector.dart +++ b/lib/src/selector.dart @@ -1,3 +1,4 @@ import 'package:json_path/fun_sdk.dart'; -typedef Selector = Nodes Function(Node node); +typedef Selector = NodeList Function(Node node); +typedef SingularSelector = SingularNodeList Function(Node node); diff --git a/pubspec.yaml b/pubspec.yaml index 225435c..1d11891 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,5 +1,5 @@ name: json_path -version: 0.6.6 +version: 0.7.0 description: Implementation of JSONPath expressions like "$.store.book[2].price". Reads and writes values in parsed JSON objects. homepage: "https://github.com/f3ath/jessie" @@ -7,16 +7,16 @@ environment: sdk: '>=3.0.0 <4.0.0' dependencies: - rfc_6901: ^0.2.0 - petitparser: ^6.0.1 - maybe_just_nothing: ^0.5.2 iregexp: ^0.1.2 + maybe_just_nothing: ^0.5.2 + petitparser: ^6.0.1 + rfc_6901: ^0.2.0 dev_dependencies: + check_coverage: ^0.0.4 lints: ^3.0.0 - test: ^1.21.1 path: ^1.8.2 - check_coverage: ^0.0.4 + test: ^1.21.1 cider: link_template: diff --git a/test/cases/cts b/test/cases/cts index ca076c0..446336c 160000 --- a/test/cases/cts +++ b/test/cases/cts @@ -1 +1 @@ -Subproject commit ca076c0e55d378236e7b70fc8e6414234dcca294 +Subproject commit 446336cd6651586f416a3b546c70bdd0fa2022c0 diff --git a/test/cases/extra/cases.json b/test/cases/extra/cases.json index 6a3222d..108d3ee 100644 --- a/test/cases/extra/cases.json +++ b/test/cases/extra/cases.json @@ -1,31 +1,31 @@ { "tests": [ { - "name": "reverse()", + "name": "reverse(@)", "selector" : "$[?reverse(@)=='cba']", "document" : ["abc", "cba"], "result": ["abc"] }, { - "name": "reverse(reverse())", + "name": "reverse(reverse(@))", "selector" : "$[?reverse(reverse(@))=='cba']", "document" : ["abc", "cba"], "result": ["cba"] }, { - "name": "reverse(reverse())", + "name": "count(siblings(@))", "selector" : "$..[?count(siblings(@)) == 1]", "document" : {"a": {"b": "x", "d": "x"}}, "result": ["x", "x"] }, { - "name": "is_object", + "name": "is_object(@)", "selector" : "$[?is_object(@)]", "document" : [1, true, {}, [42], "foo", {"a": "b"}], "result": [{}, {"a": "b"}] }, { - "name": "is_array", + "name": "is_array(@)", "selector" : "$[?is_array(@)]", "document" : [1, true, {}, [42], "foo", {"a": "b"}], "result": [[42]] @@ -53,6 +53,12 @@ "selector" : "$[?xor((@.b), (@.a))]", "document" : [{"a": 0}, {"a": 0, "b": 0}, {"b": 0}, {}], "result": [{"a": 0}, {"b": 0}] + }, + { + "name": "nodes to logical conversion in function arg", + "selector" : "$[?xor(@.*, @.*)]", + "document" : [{"a": 0}, {"a": 0, "b": 0}, {"b": 0}, {}], + "result": [] } ] } \ No newline at end of file