├── .pubignore ├── .gitmodules ├── lib ├── src │ ├── selector.dart │ ├── grammar │ │ ├── union_selector.dart │ │ ├── dot_name.dart │ │ ├── select_all_recursively.dart │ │ ├── array_index_selector.dart │ │ ├── filter_selector.dart │ │ ├── wildcard.dart │ │ ├── array_index.dart │ │ ├── negation.dart │ │ ├── array_slice_selector.dart │ │ ├── negatable.dart │ │ ├── fun_name.dart │ │ ├── child_selector.dart │ │ ├── comparison_expression.dart │ │ ├── literal.dart │ │ ├── array_slice.dart │ │ ├── sequence_selector.dart │ │ ├── singular_segment_sequence.dart │ │ ├── number.dart │ │ ├── slice_indices.dart │ │ ├── parser_ext.dart │ │ ├── compare.dart │ │ ├── strings.dart │ │ └── json_path.dart │ ├── fun │ │ ├── fun_call.dart │ │ ├── extra │ │ │ ├── xor.dart │ │ │ ├── is_array.dart │ │ │ ├── is_number.dart │ │ │ ├── is_object.dart │ │ │ ├── is_boolean.dart │ │ │ ├── is_string.dart │ │ │ ├── reverse.dart │ │ │ ├── siblings.dart │ │ │ ├── key.dart │ │ │ └── index.dart │ │ ├── standard │ │ │ ├── value.dart │ │ │ ├── match.dart │ │ │ ├── count.dart │ │ │ ├── search.dart │ │ │ ├── length.dart │ │ │ └── string_matcher.dart │ │ ├── fun.dart │ │ ├── fun_validator.dart │ │ └── fun_factory.dart │ ├── normalized │ │ ├── index_selector.dart │ │ └── name_selector.dart │ ├── json_path_match.dart │ ├── expression │ │ ├── nodes.dart │ │ ├── static_expression.dart │ │ └── expression.dart │ ├── json_path.dart │ ├── json_path_internal.dart │ ├── node_match.dart │ ├── json_path_parser.dart │ └── node.dart ├── json_path.dart ├── fun_sdk.dart └── fun_extra.dart ├── test ├── cases │ ├── extra │ │ ├── is_string.json │ │ ├── is_array.json │ │ ├── is_number.json │ │ ├── count.json │ │ ├── is_object.json │ │ ├── is_boolean.json │ │ ├── reverse.json │ │ ├── user_cases.json │ │ ├── xor.json │ │ ├── index.json │ │ └── key.json │ └── standard │ │ ├── fun_match.json │ │ ├── expressions_boolean.json │ │ ├── fun_search.json │ │ ├── expressions_ordering.json │ │ ├── normalized_path.json │ │ ├── whitespace.json │ │ ├── escaping.json │ │ ├── basic.json │ │ └── expressions_equality.json ├── node_test.dart ├── cases_test.dart ├── store.json ├── expression_test.dart ├── json_path_test.dart ├── parser_test.dart ├── functions_test.dart ├── fun_factory_test.dart └── helper.dart ├── analysis_options.yaml ├── .gitignore ├── .github ├── workflows │ ├── publish.yml │ └── build.yml └── dependabot.yml ├── pubspec.yaml ├── example ├── custom_functions.dart └── example.dart ├── LICENSE ├── benchmark └── parsing.dart ├── CONTRIBUTING.md ├── CODE_OF_CONDUCT.md ├── README.md └── CHANGELOG.md /.pubignore: -------------------------------------------------------------------------------- 1 | test/cases/ 2 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "test/cases/cts"] 2 | path = test/cases/cts 3 | url = https://github.com/jsonpath-standard/jsonpath-compliance-test-suite.git 4 | -------------------------------------------------------------------------------- /lib/src/selector.dart: -------------------------------------------------------------------------------- 1 | import 'package:json_path/fun_sdk.dart'; 2 | 3 | typedef Selector = NodeList Function(Node node); 4 | typedef SingularSelector = SingularNodeList Function(Node node); 5 | -------------------------------------------------------------------------------- /lib/src/grammar/union_selector.dart: -------------------------------------------------------------------------------- 1 | import 'package:json_path/src/selector.dart'; 2 | 3 | Selector unionSelector(Iterable selectors) => 4 | (node) => selectors.expand((s) => s(node)); 5 | -------------------------------------------------------------------------------- /lib/src/grammar/dot_name.dart: -------------------------------------------------------------------------------- 1 | import 'package:json_path/src/grammar/strings.dart'; 2 | import 'package:petitparser/petitparser.dart'; 3 | 4 | final dotName = memberNameShorthand.skip(before: char('.')); 5 | -------------------------------------------------------------------------------- /lib/src/fun/fun_call.dart: -------------------------------------------------------------------------------- 1 | import 'package:json_path/src/expression/expression.dart'; 2 | 3 | class FunCall { 4 | FunCall(this.name, this.args); 5 | 6 | final String name; 7 | final List args; 8 | } 9 | -------------------------------------------------------------------------------- /lib/json_path.dart: -------------------------------------------------------------------------------- 1 | /// JSONPath for Dart 2 | library; 3 | 4 | export 'package:json_path/src/json_path.dart'; 5 | export 'package:json_path/src/json_path_match.dart'; 6 | export 'package:json_path/src/json_path_parser.dart'; 7 | -------------------------------------------------------------------------------- /lib/src/grammar/select_all_recursively.dart: -------------------------------------------------------------------------------- 1 | import 'package:json_path/src/node.dart'; 2 | 3 | Iterable selectAllRecursively(Node node) sync* { 4 | yield node; 5 | yield* node.children.expand(selectAllRecursively); 6 | } 7 | -------------------------------------------------------------------------------- /lib/src/normalized/index_selector.dart: -------------------------------------------------------------------------------- 1 | /// Normalized index selector. 2 | class IndexSelector { 3 | IndexSelector(this.index); 4 | 5 | final int index; 6 | 7 | @override 8 | String toString() => '[$index]'; 9 | } 10 | -------------------------------------------------------------------------------- /test/cases/extra/is_string.json: -------------------------------------------------------------------------------- 1 | { 2 | "tests": [ 3 | { 4 | "name": "is_string", 5 | "selector" : "$[?is_string(@)]", 6 | "document" : [1, true, {}, [42], "foo", {"a": "b"}], 7 | "result": ["foo"] 8 | } 9 | ] 10 | } -------------------------------------------------------------------------------- /lib/src/grammar/array_index_selector.dart: -------------------------------------------------------------------------------- 1 | import 'package:json_path/fun_sdk.dart'; 2 | import 'package:json_path/src/selector.dart'; 3 | 4 | SingularSelector arrayIndexSelector(int offset) => 5 | (node) => SingularNodeList.from(node.element(offset)); 6 | -------------------------------------------------------------------------------- /lib/src/grammar/filter_selector.dart: -------------------------------------------------------------------------------- 1 | import 'package:json_path/src/expression/expression.dart'; 2 | import 'package:json_path/src/selector.dart'; 3 | 4 | Selector filterSelector(Expression filter) => 5 | (node) => node.children.where(filter.call); 6 | -------------------------------------------------------------------------------- /lib/src/grammar/wildcard.dart: -------------------------------------------------------------------------------- 1 | import 'package:json_path/src/grammar/parser_ext.dart'; 2 | import 'package:json_path/src/node.dart'; 3 | import 'package:petitparser/petitparser.dart'; 4 | 5 | final wildcard = char('*').value((Node node) => node.children); 6 | -------------------------------------------------------------------------------- /test/cases/extra/is_array.json: -------------------------------------------------------------------------------- 1 | 2 | { 3 | "tests": [ 4 | { 5 | "name": "is_array(@)", 6 | "selector" : "$[?is_array(@)]", 7 | "document" : [1, true, {}, [42], "foo", {"a": "b"}], 8 | "result": [[42]] 9 | } 10 | ] 11 | } -------------------------------------------------------------------------------- /analysis_options.yaml: -------------------------------------------------------------------------------- 1 | include: package:lints/recommended.yaml 2 | linter: 3 | rules: 4 | - sort_constructors_first 5 | - sort_unnamed_constructors_first 6 | - prefer_const_constructors 7 | - prefer_const_declarations 8 | - unnecessary_const 9 | -------------------------------------------------------------------------------- /test/cases/extra/is_number.json: -------------------------------------------------------------------------------- 1 | { 2 | "tests": [ 3 | { 4 | "name": "is_number", 5 | "selector" : "$[?is_number(@)]", 6 | "document" : [1, true, {}, [42], 3.14, "foo", {"a": "b"}], 7 | "result": [1, 3.14] 8 | } 9 | ] 10 | } -------------------------------------------------------------------------------- /lib/src/grammar/array_index.dart: -------------------------------------------------------------------------------- 1 | import 'package:json_path/src/grammar/array_index_selector.dart'; 2 | import 'package:json_path/src/grammar/number.dart'; 3 | import 'package:petitparser/petitparser.dart'; 4 | 5 | final arrayIndex = integer.map(arrayIndexSelector); 6 | -------------------------------------------------------------------------------- /test/cases/extra/count.json: -------------------------------------------------------------------------------- 1 | { 2 | "tests": [ 3 | { 4 | "name": "count(siblings(@))", 5 | "selector" : "$..[?count(siblings(@)) == 1]", 6 | "document" : {"a": {"b": "x", "d": "x"}}, 7 | "result": ["x", "x"] 8 | } 9 | ] 10 | } -------------------------------------------------------------------------------- /test/cases/extra/is_object.json: -------------------------------------------------------------------------------- 1 | { 2 | "tests": [ 3 | { 4 | "name": "is_object(@)", 5 | "selector" : "$[?is_object(@)]", 6 | "document" : [1, true, {}, [42], "foo", {"a": "b"}], 7 | "result": [{}, {"a": "b"}] 8 | } 9 | ] 10 | } -------------------------------------------------------------------------------- /test/cases/extra/is_boolean.json: -------------------------------------------------------------------------------- 1 | { 2 | "tests": [ 3 | { 4 | "name": "is_boolean", 5 | "selector" : "$[?is_boolean(@)]", 6 | "document" : [1, true, {}, [42], "foo", {"a": "b"}, false], 7 | "result": [true, false] 8 | } 9 | ] 10 | } -------------------------------------------------------------------------------- /lib/src/fun/extra/xor.dart: -------------------------------------------------------------------------------- 1 | import 'package:json_path/fun_sdk.dart'; 2 | 3 | class Xor implements Fun2 { 4 | const Xor(); 5 | 6 | @override 7 | final name = 'xor'; 8 | 9 | @override 10 | bool call(bool first, bool second) => first ^ second; 11 | } 12 | -------------------------------------------------------------------------------- /lib/src/grammar/negation.dart: -------------------------------------------------------------------------------- 1 | import 'package:json_path/src/expression/expression.dart'; 2 | import 'package:petitparser/parser.dart'; 3 | 4 | Parser> negation(Parser> p) => 5 | p.skip(before: char('!').trim()).map((expr) => expr.map((v) => !v)); 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://www.dartlang.org/guides/libraries/private-files 2 | 3 | # Files and directories created by pub 4 | .dart_tool/ 5 | .packages 6 | # If you're building an application, you may want to check-in your pubspec.lock 7 | pubspec.lock 8 | 9 | # test_coverage 10 | .coverage 11 | -------------------------------------------------------------------------------- /lib/fun_sdk.dart: -------------------------------------------------------------------------------- 1 | /// An SDK for building custom functions for JSONPath. 2 | library; 3 | 4 | export 'package:json_path/src/expression/nodes.dart'; 5 | export 'package:json_path/src/fun/fun.dart'; 6 | export 'package:json_path/src/node.dart'; 7 | export 'package:maybe_just_nothing/maybe_just_nothing.dart'; 8 | -------------------------------------------------------------------------------- /lib/src/grammar/array_slice_selector.dart: -------------------------------------------------------------------------------- 1 | import 'package:json_path/src/selector.dart'; 2 | 3 | Selector arraySliceSelector({int? start, int? stop, int? step}) => 4 | (node) sync* { 5 | final slice = node.slice(start: start, stop: stop, step: step); 6 | if (slice != null) yield* slice; 7 | }; 8 | -------------------------------------------------------------------------------- /lib/src/grammar/negatable.dart: -------------------------------------------------------------------------------- 1 | import 'package:json_path/src/expression/expression.dart'; 2 | import 'package:json_path/src/grammar/negation.dart'; 3 | import 'package:petitparser/petitparser.dart'; 4 | 5 | Parser> negatable(Parser> p) => 6 | [negation(p), p].toChoiceParser(); 7 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish to pub.dev 2 | 3 | on: 4 | push: 5 | tags: 6 | - '[0-9]+.[0-9]+.[0-9]+*' 7 | 8 | jobs: 9 | publish: 10 | permissions: 11 | id-token: write # Required for authentication using OIDC 12 | uses: dart-lang/setup-dart/.github/workflows/publish.yml@v1 13 | -------------------------------------------------------------------------------- /lib/src/fun/extra/is_array.dart: -------------------------------------------------------------------------------- 1 | import 'package:json_path/fun_sdk.dart'; 2 | 3 | /// Returns true if the value is a JSON array. 4 | class IsArray implements Fun1 { 5 | const IsArray(); 6 | 7 | @override 8 | final name = 'is_array'; 9 | 10 | @override 11 | bool call(v) => v.map((v) => v is List).or(false); 12 | } 13 | -------------------------------------------------------------------------------- /lib/src/fun/extra/is_number.dart: -------------------------------------------------------------------------------- 1 | import 'package:json_path/fun_sdk.dart'; 2 | 3 | /// Returns true if the value is a JSON number. 4 | class IsNumber implements Fun1 { 5 | const IsNumber(); 6 | 7 | @override 8 | final name = 'is_number'; 9 | 10 | @override 11 | bool call(v) => v.map((v) => v is num).or(false); 12 | } 13 | -------------------------------------------------------------------------------- /lib/src/fun/extra/is_object.dart: -------------------------------------------------------------------------------- 1 | import 'package:json_path/fun_sdk.dart'; 2 | 3 | /// Returns true if the value is a JSON object. 4 | class IsObject implements Fun1 { 5 | const IsObject(); 6 | 7 | @override 8 | final name = 'is_object'; 9 | 10 | @override 11 | bool call(v) => v.map((v) => v is Map).or(false); 12 | } 13 | -------------------------------------------------------------------------------- /lib/src/grammar/fun_name.dart: -------------------------------------------------------------------------------- 1 | import 'package:petitparser/petitparser.dart'; 2 | 3 | final _funNameFirst = lowercase(); 4 | final _funNameChar = [char('_'), digit(), lowercase()].toChoiceParser(); 5 | 6 | /// Function name. 7 | final funName = (_funNameFirst & _funNameChar.star()).flatten( 8 | message: 'function name expected', 9 | ); 10 | -------------------------------------------------------------------------------- /lib/src/json_path_match.dart: -------------------------------------------------------------------------------- 1 | import 'package:rfc_6901/rfc_6901.dart'; 2 | 3 | abstract interface class JsonPathMatch { 4 | /// The matched value. 5 | Object? get value; 6 | 7 | /// The normalized JSONPath to this node. 8 | String get path; 9 | 10 | /// JSON Pointer (RFC 6901) to this node. 11 | JsonPointer get pointer; 12 | } 13 | -------------------------------------------------------------------------------- /lib/src/fun/extra/is_boolean.dart: -------------------------------------------------------------------------------- 1 | import 'package:json_path/fun_sdk.dart'; 2 | 3 | /// Returns true if the value is a JSON boolean. 4 | class IsBoolean implements Fun1 { 5 | const IsBoolean(); 6 | 7 | @override 8 | final name = 'is_boolean'; 9 | 10 | @override 11 | bool call(v) => v.map((v) => v is bool).or(false); 12 | } 13 | -------------------------------------------------------------------------------- /lib/src/fun/extra/is_string.dart: -------------------------------------------------------------------------------- 1 | import 'package:json_path/fun_sdk.dart'; 2 | 3 | /// Returns true if the value is a JSON string. 4 | class IsString implements Fun1 { 5 | const IsString(); 6 | 7 | @override 8 | final name = 'is_string'; 9 | 10 | @override 11 | bool call(v) => v.map((v) => v is String).or(false); 12 | } 13 | -------------------------------------------------------------------------------- /lib/src/fun/extra/reverse.dart: -------------------------------------------------------------------------------- 1 | import 'package:json_path/fun_sdk.dart'; 2 | 3 | /// Reverses the string. 4 | class Reverse implements Fun1, Maybe> { 5 | const Reverse(); 6 | 7 | @override 8 | final name = 'reverse'; 9 | 10 | @override 11 | Maybe call(Maybe v) => 12 | v.type().map((s) => s.split('').reversed.join()); 13 | } 14 | -------------------------------------------------------------------------------- /test/node_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:json_path/src/node.dart'; 2 | import 'package:test/expect.dart'; 3 | import 'package:test/scaffolding.dart'; 4 | 5 | void main() { 6 | group('Node', () { 7 | test('equality', () { 8 | expect({Node(42), Node(42)}.length, equals(1)); 9 | }); 10 | test('toString()', () { 11 | expect('${Node(42)}', equals('Node(42)')); 12 | }); 13 | }); 14 | } 15 | -------------------------------------------------------------------------------- /test/cases/extra/reverse.json: -------------------------------------------------------------------------------- 1 | { 2 | "tests": [ 3 | { 4 | "name": "reverse(@)", 5 | "selector" : "$[?reverse(@)=='cba']", 6 | "document" : ["abc", "cba"], 7 | "result": ["abc"] 8 | }, 9 | { 10 | "name": "reverse(reverse(@))", 11 | "selector" : "$[?reverse(reverse(@))=='cba']", 12 | "document" : ["abc", "cba"], 13 | "result": ["cba"] 14 | } 15 | ] 16 | } -------------------------------------------------------------------------------- /lib/src/fun/extra/siblings.dart: -------------------------------------------------------------------------------- 1 | import 'package:json_path/fun_sdk.dart'; 2 | 3 | /// Returns all siblings of the given nodes. 4 | class Siblings implements Fun1 { 5 | const Siblings(); 6 | 7 | @override 8 | final name = 'siblings'; 9 | 10 | @override 11 | NodeList call(NodeList nodes) => nodes.expand( 12 | (node) => node.parent?.children.where((it) => node != it) ?? [], 13 | ); 14 | } 15 | -------------------------------------------------------------------------------- /lib/src/fun/standard/value.dart: -------------------------------------------------------------------------------- 1 | import 'package:json_path/fun_sdk.dart'; 2 | 3 | /// The standard `value()` function. 4 | /// See https://ietf-wg-jsonpath.github.io/draft-ietf-jsonpath-base/draft-ietf-jsonpath-base.html#name-value-function-extension 5 | class Value implements Fun1 { 6 | const Value(); 7 | 8 | @override 9 | final name = 'value'; 10 | 11 | @override 12 | Maybe call(NodeList arg) => arg.asValue; 13 | } 14 | -------------------------------------------------------------------------------- /lib/src/grammar/child_selector.dart: -------------------------------------------------------------------------------- 1 | import 'package:json_path/fun_sdk.dart'; 2 | import 'package:json_path/src/selector.dart'; 3 | 4 | SingularSelector childSelector(String key) { 5 | if (key.runes.any( 6 | (r) => r < 0 || r > 0x10FFFF || (r >= 0xD800 && r <= 0xDFFF), 7 | )) { 8 | throw const FormatException('Invalid UTF code units in childSelector.'); 9 | } 10 | return (node) => SingularNodeList.from(node.child(key)); 11 | } 12 | -------------------------------------------------------------------------------- /test/cases/standard/fun_match.json: -------------------------------------------------------------------------------- 1 | { 2 | "tests": [ 3 | { 4 | "name": "dot matcher on \\u2028", 5 | "selector": "$[?match(@, '.')]", 6 | "document": ["\u2028", "\r", "\n", true, [], {}], 7 | "result": ["\u2028"] 8 | }, 9 | { 10 | "name": "dot matcher on \\u2029", 11 | "selector": "$[?match(@, '.')]", 12 | "document": ["\u2029", "\r", "\n", true, [], {}], 13 | "result": ["\u2029"] 14 | } 15 | ] 16 | } -------------------------------------------------------------------------------- /test/cases/extra/user_cases.json: -------------------------------------------------------------------------------- 1 | { 2 | "tests": [ 3 | { 4 | "name": "https://github.com/f3ath/jessie/issues/103", 5 | "selector" : "$[?match(key(@), 'Serial .*')]", 6 | "document" : { 7 | "ynid": "AX0086HHN", 8 | "Serial Group": "009722918001826A", 9 | "Serial Name": "foo", 10 | "Serial Number": "bar", 11 | "serial whatever": "baz" 12 | }, 13 | "result": ["009722918001826A", "foo", "bar"] 14 | } 15 | ] 16 | } -------------------------------------------------------------------------------- /test/cases/extra/xor.json: -------------------------------------------------------------------------------- 1 | { 2 | "tests": [ 3 | { 4 | "name": "parens in functional args", 5 | "selector" : "$[?xor((@.b), (@.a))]", 6 | "document" : [{"a": 0}, {"a": 0, "b": 0}, {"b": 0}, {}], 7 | "result": [{"a": 0}, {"b": 0}] 8 | }, 9 | { 10 | "name": "nodes to logical conversion in function arg", 11 | "selector" : "$[?xor(@.*, @.*)]", 12 | "document" : [{"a": 0}, {"a": 0, "b": 0}, {"b": 0}, {}], 13 | "result": [] 14 | } 15 | ] 16 | } -------------------------------------------------------------------------------- /test/cases/extra/index.json: -------------------------------------------------------------------------------- 1 | { 2 | "tests": [ 3 | { 4 | "name": "index()", 5 | "selector" : "$[?index(@) == 1]", 6 | "document" : [ "A", "B"], 7 | "result": ["B"] 8 | }, 9 | { 10 | "name": "index(), does not work on objects", 11 | "selector" : "$[?index(@) == 0]", 12 | "document" : {"0": "A", "1": "B"}, 13 | "result": [] 14 | }, 15 | { 16 | "name": "index(), non singular", 17 | "selector" : "$[?index(@.*) == 0]", 18 | "invalid_selector": true 19 | } 20 | ] 21 | } -------------------------------------------------------------------------------- /lib/src/grammar/comparison_expression.dart: -------------------------------------------------------------------------------- 1 | import 'package:json_path/src/expression/expression.dart'; 2 | import 'package:json_path/src/grammar/compare.dart'; 3 | import 'package:maybe_just_nothing/maybe_just_nothing.dart'; 4 | import 'package:petitparser/parser.dart'; 5 | 6 | Parser> comparisonExpression(Parser> val) => 7 | (val & cmpOperator & val).map((v) { 8 | final [Expression left, String op, Expression right] = v; 9 | return left.merge(right, (l, r) => compare(op, l, r)); 10 | }); 11 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "pub" # See documentation for possible values 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "weekly" 12 | -------------------------------------------------------------------------------- /lib/src/fun/extra/key.dart: -------------------------------------------------------------------------------- 1 | import 'package:json_path/fun_sdk.dart'; 2 | 3 | /// Returns the key under which the node referenced by the argument 4 | /// is found in the parent object. 5 | /// If the parent is not an object, returns [Nothing]. 6 | /// If the argument does not reference a single node, returns [Nothing]. 7 | class Key implements Fun1, SingularNodeList> { 8 | const Key(); 9 | 10 | @override 11 | final name = 'key'; 12 | 13 | @override 14 | Maybe call(SingularNodeList nodes) => 15 | Just(nodes.node?.key).type(); 16 | } 17 | -------------------------------------------------------------------------------- /lib/src/fun/extra/index.dart: -------------------------------------------------------------------------------- 1 | import 'package:json_path/fun_sdk.dart'; 2 | 3 | /// Returns the index under which the node referenced by the argument 4 | /// is found in the parent array. 5 | /// If the parent is not an array, returns [Nothing]. 6 | /// If the argument does not reference a single node, returns [Nothing]. 7 | class Index implements Fun1, SingularNodeList> { 8 | const Index(); 9 | 10 | @override 11 | final name = 'index'; 12 | 13 | @override 14 | Maybe call(SingularNodeList nodes) => 15 | Just(nodes.node?.index).type(); 16 | } 17 | -------------------------------------------------------------------------------- /lib/src/fun/standard/match.dart: -------------------------------------------------------------------------------- 1 | import 'package:json_path/src/fun/standard/string_matcher.dart'; 2 | 3 | /// The standard `match()` function which returns `true` 4 | /// if the value matches the regex. 5 | /// The regex must be a valid [I-Regexp](https://datatracker.ietf.org/doc/draft-ietf-jsonpath-iregexp/) 6 | /// expression, otherwise the function returns `false` regardless of the value. 7 | /// See https://ietf-wg-jsonpath.github.io/draft-ietf-jsonpath-base/draft-ietf-jsonpath-base.html#name-match-function-extension 8 | class Match extends StringMatcher { 9 | const Match() : super('match', false); 10 | } 11 | -------------------------------------------------------------------------------- /lib/src/fun/standard/count.dart: -------------------------------------------------------------------------------- 1 | import 'package:json_path/src/expression/nodes.dart'; 2 | import 'package:json_path/src/fun/fun.dart'; 3 | import 'package:maybe_just_nothing/maybe_just_nothing.dart'; 4 | 5 | /// The standard `count()` function which returns the number of nodes in a node list. 6 | /// See https://ietf-wg-jsonpath.github.io/draft-ietf-jsonpath-base/draft-ietf-jsonpath-base.html#name-count-function-extension 7 | class Count implements Fun1, NodeList> { 8 | const Count(); 9 | 10 | @override 11 | final name = 'count'; 12 | 13 | @override 14 | Maybe call(NodeList nodes) => Just(nodes.length); 15 | } 16 | -------------------------------------------------------------------------------- /lib/src/fun/standard/search.dart: -------------------------------------------------------------------------------- 1 | import 'package:json_path/src/fun/standard/string_matcher.dart'; 2 | 3 | /// The standard `search()` function which returns `true` if 4 | /// the value contains a substring which matches the regex. 5 | /// The regex must be a valid [I-Regexp](https://datatracker.ietf.org/doc/draft-ietf-jsonpath-iregexp/) 6 | /// expression, otherwise the function returns `false` regardless of the value. 7 | /// See https://ietf-wg-jsonpath.github.io/draft-ietf-jsonpath-base/draft-ietf-jsonpath-base.html#name-search-function-extension 8 | class Search extends StringMatcher { 9 | const Search() : super('search', true); 10 | } 11 | -------------------------------------------------------------------------------- /lib/src/fun/standard/length.dart: -------------------------------------------------------------------------------- 1 | import 'package:json_path/fun_sdk.dart'; 2 | 3 | /// The standard `length()` function. 4 | /// See https://ietf-wg-jsonpath.github.io/draft-ietf-jsonpath-base/draft-ietf-jsonpath-base.html#name-length-function-extension 5 | class Length implements Fun1, Maybe> { 6 | const Length(); 7 | 8 | @override 9 | final name = 'length'; 10 | 11 | @override 12 | Maybe call(Maybe value) => value 13 | .type() 14 | .map((it) => it.length) 15 | .fallback(() => value.type().map((it) => it.length)) 16 | .fallback(() => value.type().map((it) => it.length)); 17 | } 18 | -------------------------------------------------------------------------------- /lib/src/grammar/literal.dart: -------------------------------------------------------------------------------- 1 | import 'package:json_path/src/expression/expression.dart'; 2 | import 'package:json_path/src/expression/static_expression.dart'; 3 | import 'package:json_path/src/grammar/number.dart'; 4 | import 'package:json_path/src/grammar/strings.dart'; 5 | import 'package:maybe_just_nothing/maybe_just_nothing.dart'; 6 | import 'package:petitparser/parser.dart'; 7 | 8 | final Parser> literal = [ 9 | string('null').map((_) => null), 10 | string('false').map((_) => false), 11 | string('true').map((_) => true), 12 | number, 13 | quotedString, 14 | ].toChoiceParser().map(Just.new).map(StaticExpression.new); 15 | -------------------------------------------------------------------------------- /test/cases_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:json_path/fun_extra.dart'; 2 | import 'package:json_path/json_path.dart'; 3 | 4 | import 'helper.dart'; 5 | 6 | void main() { 7 | runTestsInDirectory('test/cases/cts'); 8 | runTestsInDirectory('test/cases/standard'); 9 | runTestsInDirectory( 10 | 'test/cases/extra', 11 | parser: JsonPathParser( 12 | functions: const [ 13 | Index(), 14 | IsArray(), 15 | IsBoolean(), 16 | IsNumber(), 17 | IsObject(), 18 | IsString(), 19 | Key(), 20 | Reverse(), 21 | Siblings(), 22 | Xor(), 23 | ], 24 | ), 25 | ); 26 | } 27 | -------------------------------------------------------------------------------- /lib/src/grammar/array_slice.dart: -------------------------------------------------------------------------------- 1 | import 'package:json_path/src/grammar/array_slice_selector.dart'; 2 | import 'package:json_path/src/grammar/number.dart'; 3 | import 'package:petitparser/petitparser.dart'; 4 | 5 | final _colon = char(':').trim(); 6 | 7 | final _optionalInt = integer.optional(); 8 | 9 | final arraySlice = 10 | (_optionalInt & 11 | _optionalInt.skip(before: _colon) & 12 | _optionalInt.skip(before: _colon).optional()) 13 | .map( 14 | (value) => arraySliceSelector( 15 | start: value[0], 16 | stop: value[1], 17 | step: value[2], 18 | ), 19 | ); 20 | -------------------------------------------------------------------------------- /lib/fun_extra.dart: -------------------------------------------------------------------------------- 1 | /// A collection of semi-useful non-standard functions for JSONPath. 2 | library; 3 | 4 | export 'package:json_path/src/fun/extra/index.dart'; 5 | export 'package:json_path/src/fun/extra/is_array.dart'; 6 | export 'package:json_path/src/fun/extra/is_boolean.dart'; 7 | export 'package:json_path/src/fun/extra/is_number.dart'; 8 | export 'package:json_path/src/fun/extra/is_object.dart'; 9 | export 'package:json_path/src/fun/extra/is_string.dart'; 10 | export 'package:json_path/src/fun/extra/key.dart'; 11 | export 'package:json_path/src/fun/extra/reverse.dart'; 12 | export 'package:json_path/src/fun/extra/siblings.dart'; 13 | export 'package:json_path/src/fun/extra/xor.dart'; 14 | -------------------------------------------------------------------------------- /lib/src/expression/nodes.dart: -------------------------------------------------------------------------------- 1 | import 'package:json_path/src/node.dart'; 2 | import 'package:maybe_just_nothing/maybe_just_nothing.dart'; 3 | 4 | typedef NodeList = Iterable; 5 | 6 | class SingularNodeList with NodeList { 7 | SingularNodeList(this._nodes); 8 | 9 | SingularNodeList.from(Node? node) 10 | : this(node != null ? [node] : const .empty()); 11 | 12 | final NodeList _nodes; 13 | 14 | Node? get node => length == 1 ? first : null; 15 | 16 | @override 17 | Iterator> get iterator => _nodes.iterator; 18 | } 19 | 20 | extension NodeListExt on NodeList { 21 | Maybe get asValue => length == 1 ? Just(single.value) : const Nothing(); 22 | 23 | bool get asLogical => isNotEmpty; 24 | } 25 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: json_path 2 | version: 0.9.0 3 | description: "Implementation of RFC 9535 - JSONPath: Query Expressions for JSON. Reads and writes values in parsed JSON objects using queries like `$.store.book[2].price`." 4 | homepage: "https://github.com/f3ath/jessie" 5 | 6 | environment: 7 | sdk: ^3.10.0 8 | 9 | dependencies: 10 | iregexp: ^0.2.0 11 | maybe_just_nothing: ^0.6.0 12 | petitparser: ^7.0.0 13 | rfc_6901: ^0.2.0 14 | 15 | dev_dependencies: 16 | check_coverage: ^0.0.4 17 | lints: ^6.0.0 18 | path: ^1.8.2 19 | test: ^1.21.1 20 | 21 | cider: 22 | link_template: 23 | tag: https://github.com/f3ath/jessie/releases/tag/%tag% 24 | diff: https://github.com/f3ath/jessie/compare/%from%...%to% 25 | -------------------------------------------------------------------------------- /lib/src/expression/static_expression.dart: -------------------------------------------------------------------------------- 1 | import 'package:json_path/src/expression/expression.dart'; 2 | 3 | /// A special case of [Expression] where the value is known at parse time. 4 | class StaticExpression extends Expression { 5 | StaticExpression(this.value) : super((_) => value); 6 | 7 | final T value; 8 | 9 | @override 10 | Expression map(R Function(T v) mapper) => 11 | StaticExpression(mapper(value)); 12 | 13 | @override 14 | Expression merge( 15 | Expression other, 16 | R Function(T v, M m) merger, 17 | ) => other is StaticExpression 18 | ? StaticExpression(merger(value, other.value)) 19 | : super.merge(other, merger); 20 | } 21 | -------------------------------------------------------------------------------- /test/cases/extra/key.json: -------------------------------------------------------------------------------- 1 | { 2 | "tests": [ 3 | { 4 | "name": "key()", 5 | "selector" : "$[?key(@) == 'a']", 6 | "document" : {"a": "A", "b": "B"}, 7 | "result": ["A"] 8 | }, 9 | { 10 | "name": "key(), palindromic keys", 11 | "selector" : "$[?key(@) == reverse(key(@))]", 12 | "document" : {"foo": "FOO", "bar": "BAR", "bab": "BAB", "": "", "a": "A"}, 13 | "result": ["BAB","","A"] 14 | }, 15 | { 16 | "name": "key(), does not work on arrays", 17 | "selector" : "$[?key(@) == 0]", 18 | "document" : ["A", "B"], 19 | "result": [] 20 | }, 21 | { 22 | "name": "key(), non singular", 23 | "selector" : "$[?key(@.*) == 'a']", 24 | "invalid_selector": true 25 | } 26 | ] 27 | } -------------------------------------------------------------------------------- /lib/src/grammar/sequence_selector.dart: -------------------------------------------------------------------------------- 1 | import 'package:json_path/src/expression/nodes.dart'; 2 | import 'package:json_path/src/selector.dart'; 3 | 4 | Selector sequenceSelector(Iterable selectors) => 5 | (node) => selectors.fold<_Filter>( 6 | (v) => v, 7 | (filter, selector) => 8 | (nodes) => filter(nodes).expand(selector), 9 | )([node]); 10 | 11 | SingularSelector singularSequenceSelector( 12 | Iterable selectors, 13 | ) => 14 | (node) => selectors.fold<_SingularFilter>( 15 | SingularNodeList.new, 16 | (filter, selector) => 17 | (nodes) => SingularNodeList(filter(nodes).expand(selector)), 18 | )([node]); 19 | 20 | typedef _Filter = NodeList Function(NodeList nodes); 21 | 22 | typedef _SingularFilter = SingularNodeList Function(NodeList nodes); 23 | -------------------------------------------------------------------------------- /lib/src/json_path.dart: -------------------------------------------------------------------------------- 1 | import 'package:json_path/src/json_path_match.dart'; 2 | import 'package:json_path/src/json_path_parser.dart'; 3 | 4 | /// A parsed JSONPath expression which can be applied to a JSON document. 5 | abstract interface class JsonPath { 6 | /// Creates an instance from a string. The [expression] is parsed once, and 7 | /// the instance may be used many times after that. 8 | /// 9 | /// Throws [FormatException] if the [expression] can not be parsed. 10 | factory JsonPath(String expression) => JsonPathParser().parse(expression); 11 | 12 | /// Reads the given [json] object returning an Iterable of all matches found. 13 | Iterable read(dynamic json); 14 | 15 | /// Reads the given [json] object returning an Iterable of all values found. 16 | Iterable readValues(dynamic json); 17 | } 18 | -------------------------------------------------------------------------------- /test/store.json: -------------------------------------------------------------------------------- 1 | { 2 | "store": { 3 | "book": [ 4 | { 5 | "category": "reference", 6 | "author": "Nigel Rees", 7 | "title": "Sayings of the Century", 8 | "price": 8.95 9 | }, 10 | { 11 | "category": "fiction", 12 | "author": "Evelyn Waugh", 13 | "title": "Sword of Honour", 14 | "price": 12.99 15 | }, 16 | { 17 | "category": "fiction", 18 | "author": "Herman Melville", 19 | "title": "Moby Dick", 20 | "isbn": "0-553-21311-3", 21 | "price": 8.99 22 | }, 23 | { 24 | "category": "fiction", 25 | "author": "J. R. R. Tolkien", 26 | "title": "The Lord of the Rings", 27 | "isbn": "0-395-19395-8", 28 | "price": 22.99 29 | } 30 | ], 31 | "bicycle": { 32 | "color": "red", 33 | "price": 19.95 34 | } 35 | } 36 | } -------------------------------------------------------------------------------- /lib/src/grammar/singular_segment_sequence.dart: -------------------------------------------------------------------------------- 1 | import 'package:json_path/src/expression/expression.dart'; 2 | import 'package:json_path/src/grammar/array_index.dart'; 3 | import 'package:json_path/src/grammar/child_selector.dart'; 4 | import 'package:json_path/src/grammar/dot_name.dart'; 5 | import 'package:json_path/src/grammar/parser_ext.dart'; 6 | import 'package:json_path/src/grammar/sequence_selector.dart'; 7 | import 'package:json_path/src/grammar/strings.dart'; 8 | import 'package:petitparser/petitparser.dart'; 9 | 10 | final _singularUnionElement = [ 11 | arrayIndex, 12 | quotedString.map(childSelector), 13 | ].toChoiceParser().trim(); 14 | 15 | final _singularUnion = _singularUnionElement.inBrackets(); 16 | 17 | final _singularSegment = [dotName, _singularUnion].toChoiceParser().trim(); 18 | 19 | final singularSegmentSequence = _singularSegment 20 | .star() 21 | .map(singularSequenceSelector) 22 | .map(Expression.new); 23 | -------------------------------------------------------------------------------- /lib/src/normalized/name_selector.dart: -------------------------------------------------------------------------------- 1 | /// Normalized name selector. 2 | class NameSelector { 3 | NameSelector(this.name); 4 | 5 | final String name; 6 | 7 | @override 8 | String toString() => "['${name.escaped}']"; 9 | } 10 | 11 | extension on String { 12 | /// Returns a string with all characters escaped as unicode entities. 13 | String get unicodeEscaped => 14 | codeUnits.map((c) => '\\u${c.toRadixString(16).padLeft(4, '0')}').join(); 15 | 16 | String get escaped => 17 | const { 18 | r'\': r'\\', 19 | '\b': r'\b', 20 | '\f': r'\f', 21 | '\n': r'\n', 22 | '\r': r'\r', 23 | '\t': r'\t', 24 | "'": r"\'", 25 | }.entries 26 | .fold(this, (s, e) => s.replaceAll(e.key, e.value)) 27 | .replaceAllMapped( 28 | RegExp(r'[\u0000-\u001f]'), 29 | (s) => s[0]!.unicodeEscaped, 30 | ); 31 | } 32 | -------------------------------------------------------------------------------- /lib/src/fun/standard/string_matcher.dart: -------------------------------------------------------------------------------- 1 | import 'package:iregexp/iregexp.dart'; 2 | import 'package:json_path/src/fun/fun.dart'; 3 | import 'package:maybe_just_nothing/maybe_just_nothing.dart'; 4 | 5 | abstract class StringMatcher implements Fun2 { 6 | const StringMatcher(this.name, this.allowSubstring); 7 | 8 | @override 9 | final String name; 10 | 11 | final bool allowSubstring; 12 | 13 | @override 14 | bool call(Maybe value, Maybe regex) => 15 | value.merge(regex, _typeSafeMatch).or(false); 16 | 17 | bool _typeSafeMatch(Object? value, Object? regex) { 18 | if (value is! String || regex is! String) return false; 19 | try { 20 | return _match(value, IRegexp(regex)); 21 | } on FormatException { 22 | return false; // Invalid regex means no match 23 | } 24 | } 25 | 26 | bool _match(String value, IRegexp regex) => 27 | allowSubstring ? regex.matchesSubstring(value) : regex.matches(value); 28 | } 29 | -------------------------------------------------------------------------------- /example/custom_functions.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | import 'package:json_path/fun_sdk.dart'; 4 | import 'package:json_path/json_path.dart'; 5 | 6 | /// This example shows how to create and use a custom function. 7 | void main() { 8 | final parser = JsonPathParser(functions: [IsPalindrome()]); 9 | final jsonPath = parser.parse(r'$[?is_palindrome(@)]'); 10 | final json = jsonDecode('["madam", "foo", "nurses run", 42, {"A": "B"}]'); 11 | jsonPath.readValues(json).forEach(print); 12 | } 13 | 14 | /// An implementation of a palindrome test. 15 | class IsPalindrome implements Fun1 { 16 | @override 17 | final name = 'is_palindrome'; 18 | 19 | @override 20 | bool call(Maybe arg) => arg 21 | .type() // Make sure it's a string 22 | .map((value) => value.replaceAll(' ', '')) // drop spaces 23 | .map((value) => value.split('').reversed.join() == value) // palindrome? 24 | .or(false); // for non-string values return false 25 | } 26 | -------------------------------------------------------------------------------- /test/expression_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:json_path/src/expression/expression.dart'; 2 | import 'package:json_path/src/expression/static_expression.dart'; 3 | import 'package:json_path/src/node.dart'; 4 | import 'package:maybe_just_nothing/maybe_just_nothing.dart'; 5 | import 'package:test/expect.dart'; 6 | import 'package:test/scaffolding.dart'; 7 | 8 | void main() { 9 | group('Expression', () { 10 | group('Static', () { 11 | final node = Node('foo'); 12 | final s = StaticExpression(const Just('bar')); 13 | final d = Expression((Node n) => Just(n.value)); 14 | test('map()', () { 15 | expect(s.map((v) => v.map((v) => '$v!').or('oops')).call(node), 'bar!'); 16 | }); 17 | test('merge() with non-static', () { 18 | expect( 19 | s 20 | .merge(d, (v, m) => v.merge(m, (a, b) => '$a$b').or('oops')) 21 | .call(node), 22 | 'barfoo', 23 | ); 24 | }); 25 | }); 26 | }); 27 | } 28 | -------------------------------------------------------------------------------- /lib/src/json_path_internal.dart: -------------------------------------------------------------------------------- 1 | import 'package:json_path/src/json_path.dart'; 2 | import 'package:json_path/src/json_path_match.dart'; 3 | import 'package:json_path/src/node.dart'; 4 | import 'package:json_path/src/node_match.dart'; 5 | import 'package:json_path/src/selector.dart'; 6 | 7 | /// Internal implementation of [JsonPath]. 8 | class JsonPathInternal implements JsonPath { 9 | JsonPathInternal(this.expression, this.selector); 10 | 11 | /// Selector 12 | final Selector selector; 13 | 14 | /// JSONPath expression. 15 | final String expression; 16 | 17 | /// Reads the given [json] object returning an Iterable of all matches found. 18 | @override 19 | Iterable read(json) => selector(Node(json)).map(NodeMatch.new); 20 | 21 | /// Reads the given [json] object returning an Iterable of all values found. 22 | @override 23 | Iterable readValues(json) => read(json).map((node) => node.value); 24 | 25 | @override 26 | String toString() => expression; 27 | } 28 | -------------------------------------------------------------------------------- /lib/src/grammar/number.dart: -------------------------------------------------------------------------------- 1 | import 'package:petitparser/petitparser.dart'; 2 | 3 | const _intMin = -9007199254740991; 4 | const _intMax = 9007199254740991; 5 | 6 | final _digit1 = range('1', '9'); 7 | 8 | final _integer = (char('0') | (char('-').optional() & _digit1 & digit().star())) 9 | .flatten(message: 'integer expected'); 10 | 11 | final _float = 12 | (char('-').optional() & digit().plus() & char('.') & digit().plus()) 13 | .flatten(message: 'floating point expected'); 14 | 15 | final _exp = 16 | ((_float | _integer | string('-0')) & 17 | char('e', ignoreCase: true) & 18 | anyOf('-+').optional() & 19 | digit().plus()) 20 | .flatten(message: 'an exponent number expected'); 21 | 22 | final integer = _integer 23 | .map(int.parse) 24 | .where((it) => it >= _intMin && it <= _intMax); 25 | 26 | final Parser number = [ 27 | _exp.map(double.parse), 28 | _float.map(double.parse), 29 | string('-0').map((_) => 0), 30 | integer, 31 | ].toChoiceParser(); 32 | -------------------------------------------------------------------------------- /lib/src/fun/fun.dart: -------------------------------------------------------------------------------- 1 | /// A named function which can be used in a JSONPath expression. 2 | abstract interface class Fun { 3 | /// Function name. 4 | String get name; 5 | } 6 | 7 | /// A named function with one argument. 8 | /// The return type [R] and the argument type [T] must be one of the following: 9 | /// - [bool] 10 | /// - [Maybe] 11 | /// - [NodeList] 12 | abstract interface class Fun1 extends Fun { 13 | /// Applies the given arguments. 14 | /// This method MUST throw an [Exception] on invalid args. 15 | R call(T arg); 16 | } 17 | 18 | /// A named function with two arguments. 19 | /// The return type [R] and the argument types [T1], [T2] must be one of the following: 20 | /// - [bool] 21 | /// - [Maybe] 22 | /// - [NodeList] 23 | abstract interface class Fun2< 24 | R extends Object, 25 | T1 extends Object, 26 | T2 extends Object 27 | > 28 | extends Fun { 29 | /// Applies the given arguments. 30 | /// This method MUST throw an [Exception] on invalid args. 31 | R call(T1 first, T2 second); 32 | } 33 | -------------------------------------------------------------------------------- /lib/src/expression/expression.dart: -------------------------------------------------------------------------------- 1 | import 'package:json_path/src/node.dart'; 2 | 3 | /// An expression applicable to a JSON node. For example: 4 | /// `length(@.foo) > 3 && @.bar`. The `@` denotes the current node 5 | /// being processed by JSONPath. 6 | class Expression { 7 | Expression(this.call); 8 | 9 | /// Returns the result of applying the expression to the node. 10 | final T Function(Node) call; 11 | 12 | /// Creates a new [Expression] by applying the [mapper] function 13 | /// to the result of this expression. 14 | Expression map(R Function(T v) mapper) => 15 | Expression((node) => mapper(call(node))); 16 | 17 | /// Creates a new [Expression] from the [other] [Expression] and the 18 | /// [merger] function. The [merger] function is applied to the values 19 | /// produced by this an the [other] [Expression]. 20 | Expression merge( 21 | Expression other, 22 | R Function(T a, M b) merger, 23 | ) => Expression((node) => merger(call(node), other.call(node))); 24 | } 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020-2023 Alexey Karapetov 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /lib/src/grammar/slice_indices.dart: -------------------------------------------------------------------------------- 1 | import 'dart:math'; 2 | 3 | Iterable sliceIndices(int length, int? start, int? stop, int step) sync* { 4 | if (step > 0) yield* _forward(length, start ?? 0, stop ?? length, step); 5 | if (step < 0) yield* _backward(length, start, stop, step); 6 | } 7 | 8 | Iterable _forward(int length, int start, int stop, int step) sync* { 9 | final low = start < 0 ? max(length + start, 0) : start; 10 | final high = stop < 0 ? length + stop : min(length, stop); 11 | for (var i = low; i < high; i += step) { 12 | yield i; 13 | } 14 | } 15 | 16 | Iterable _backward(int length, int? start, int? stop, int step) sync* { 17 | final low = _low(stop, length); 18 | final high = _high(start, length); 19 | for (var i = high; i > low; i += step) { 20 | yield i; 21 | } 22 | } 23 | 24 | /// exclusive 25 | int _low(int? stop, int length) => switch (stop) { 26 | null => -1, 27 | < 0 => max(length + stop, -1), 28 | _ => stop, 29 | }; 30 | 31 | /// inclusive 32 | int _high(int? start, int length) => switch (start) { 33 | null => length - 1, 34 | < 0 => length + start, 35 | _ => min(start, length - 1), 36 | }; 37 | -------------------------------------------------------------------------------- /lib/src/node_match.dart: -------------------------------------------------------------------------------- 1 | import 'package:json_path/json_path.dart'; 2 | import 'package:json_path/src/node.dart'; 3 | import 'package:json_path/src/normalized/index_selector.dart'; 4 | import 'package:json_path/src/normalized/name_selector.dart'; 5 | import 'package:rfc_6901/rfc_6901.dart'; 6 | 7 | class NodeMatch implements JsonPathMatch { 8 | NodeMatch(Node node) 9 | : value = node.value, 10 | path = node.path(), 11 | pointer = node.pointer(); 12 | 13 | @override 14 | final String path; 15 | 16 | @override 17 | final JsonPointer pointer; 18 | 19 | @override 20 | final Object? value; 21 | } 22 | 23 | extension on Node { 24 | Iterable trace() sync* { 25 | if (key != null) { 26 | yield* parent!.trace(); 27 | yield key!; 28 | } 29 | if (index != null) { 30 | yield* parent!.trace(); 31 | yield index!; 32 | } 33 | } 34 | 35 | JsonPointer pointer() => JsonPointer.build(trace().map((e) => e.toString())); 36 | 37 | String path() => r'$' + trace().map(_segment).join(); 38 | 39 | Object _segment(Object? v) => 40 | v is int ? IndexSelector(v) : NameSelector(v.toString()); 41 | } 42 | -------------------------------------------------------------------------------- /test/json_path_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:json_path/json_path.dart'; 2 | import 'package:test/test.dart'; 3 | 4 | void main() { 5 | group('JSONPath', () { 6 | test('toString()', () { 7 | const expr = r'$.foo.bar'; 8 | expect('${JsonPath(expr)}', equals(expr)); 9 | }); 10 | }); 11 | group('Invalid expressions', () { 12 | for (final expr in [ 13 | r"$(key,more)", 14 | r"$.", 15 | r"$.[key]", 16 | r"$['foo'bar']", 17 | r"$['two'.'some']", 18 | r"$[two.some]", 19 | r'$$', 20 | r'$....', 21 | r'$...foo', 22 | r'$["""]', 23 | r'$["\"]', 24 | r'$["\z"]', 25 | r'$["]', 26 | r'$["foo"bar"]', 27 | r'$[1 1]', 28 | r'$[1+2]', 29 | r'$[1874509822062987436598726432519879857164397163046130756769274369]', 30 | r'$[:::]', 31 | r'$[]', 32 | r'', 33 | r'.foo', 34 | ]) { 35 | test(expr, () { 36 | try { 37 | JsonPath(expr); 38 | fail('Expected FormatException'); 39 | } on FormatException catch (e) { 40 | expect(e.message, isNotEmpty); 41 | } 42 | }); 43 | } 44 | }); 45 | } 46 | -------------------------------------------------------------------------------- /benchmark/parsing.dart: -------------------------------------------------------------------------------- 1 | import 'package:json_path/json_path.dart'; 2 | 3 | void main() { 4 | const e1 = 5 | 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)]"; 6 | const e2 = 7 | r"$..book[0:10][?(match(@.author, '(J.K.|George R.R.) Martin') && @.price < 30 && @.published >= '2010-01-01')].summary[?(length(@) > 200)]"; 8 | const e3 = 9 | r"$..store.bicycle[?(@.price > 50)].model[?match(@, '(Mountain|Road)')]"; 10 | const e4 = 11 | r"$..orders[*][?(@.status == 'delivered' && value(@.items[0:5][?(@.price > 50 && match(@.category, '(Electronics|Books)'))].quantity) > 1)].tracking[*].history[?(@.location == 'Warehouse' && @.status == 'out for delivery')]"; 12 | 13 | final start = DateTime.now(); 14 | for (var i = 0; i < 50000; i++) { 15 | JsonPath(e1); 16 | JsonPath(e2); 17 | JsonPath(e3); 18 | JsonPath(e4); 19 | } 20 | final end = DateTime.now(); 21 | print( 22 | 'Duration: ${end.difference(start).inMilliseconds} ms'); 23 | } 24 | -------------------------------------------------------------------------------- /test/parser_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:json_path/src/fun/fun.dart'; 2 | import 'package:json_path/src/fun/fun_factory.dart'; 3 | import 'package:json_path/src/fun/standard/count.dart'; 4 | import 'package:json_path/src/fun/standard/length.dart'; 5 | import 'package:json_path/src/fun/standard/match.dart'; 6 | import 'package:json_path/src/fun/standard/search.dart'; 7 | import 'package:json_path/src/fun/standard/value.dart'; 8 | import 'package:json_path/src/grammar/json_path.dart'; 9 | import 'package:json_path/src/grammar/parser_ext.dart'; 10 | import 'package:petitparser/parser.dart'; 11 | import 'package:petitparser/reflection.dart'; 12 | import 'package:test/test.dart'; 13 | 14 | void main() { 15 | final parser = JsonPathGrammarDefinition( 16 | FunFactory([ 17 | const Length(), 18 | const Count(), 19 | const Match(), 20 | const Search(), 21 | const Value(), 22 | ]), 23 | ).build(); 24 | 25 | group('Parser', () { 26 | test('Linter is happy', () { 27 | expect(linter(parser), isEmpty); 28 | }); 29 | test('Can use copy() after tryMap()', () { 30 | expect(char('x').tryMap((x) => x + x).copy(), isA>()); 31 | }); 32 | }); 33 | } 34 | -------------------------------------------------------------------------------- /test/functions_test.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | import 'dart:io'; 3 | 4 | import 'package:json_path/fun_extra.dart'; 5 | import 'package:json_path/json_path.dart'; 6 | import 'package:test/test.dart'; 7 | 8 | void main() { 9 | final store = jsonDecode(File('test/store.json').readAsStringSync()); 10 | final parser = JsonPathParser( 11 | functions: [const Reverse(), const Siblings(), const Xor()], 12 | ); 13 | group('User-defined functions', () { 14 | test('Fun in Nodes context', () { 15 | expect( 16 | parser 17 | .parse(r'$..[?count(siblings(siblings(@))) > 4]') 18 | .readValues(store) 19 | .length, 20 | equals(22), 21 | ); 22 | }); 23 | }); 24 | 25 | group('Logical', () { 26 | test('xor', () { 27 | final json = ['', 'a', 'ab', 'abc', 'aaa', 'bob']; 28 | expect( 29 | parser 30 | .parse(r'$[?xor(search(@, "a"), search(@, "b"))]') 31 | .readValues(json) 32 | .toList(), 33 | equals(['a', 'aaa', 'bob']), 34 | ); 35 | }); 36 | }); 37 | 38 | test('function not found', () { 39 | expect(() => parser.parse(r'$[?foo(@) == 3]'), throwsFormatException); 40 | }); 41 | } 42 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Dart CI 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | container: 12 | image: dart:stable 13 | steps: 14 | - uses: actions/checkout@v2 15 | - name: Update submodules 16 | run: git config --global --add safe.directory '*' && git submodule update --init --recursive 17 | - name: Print Dart version 18 | run: dart --version 19 | - name: Install dependencies 20 | run: dart pub get 21 | - name: Formatter 22 | run: dart format --output none --set-exit-if-changed example lib test 23 | - name: Analyzer 24 | run: dart analyze --fatal-infos --fatal-warnings 25 | - name: Tests 26 | run: dart test --coverage=.coverage 27 | - name: Coverage 28 | run: dart run coverage:format_coverage -l -c -i .coverage --report-on=lib --packages=.dart_tool/package_config.json | dart run check_coverage:check_coverage 29 | downgrade: 30 | runs-on: ubuntu-latest 31 | container: 32 | image: dart:stable 33 | steps: 34 | - uses: actions/checkout@v2 35 | - name: Install dependencies 36 | run: dart pub get 37 | - name: Downgrade 38 | run: dart pub downgrade 39 | - name: Analyzer 40 | run: dart analyze --fatal-infos --fatal-warnings -------------------------------------------------------------------------------- /lib/src/grammar/parser_ext.dart: -------------------------------------------------------------------------------- 1 | import 'package:petitparser/petitparser.dart'; 2 | 3 | extension ParserExt on Parser { 4 | /// Returns a predefined value parser. 5 | Parser value(V v) => map((_) => v); 6 | 7 | /// Makes a list of this elements separated by [separator] (a comma by default). 8 | Parser> toList([Parser? separator]) => [ 9 | map((v) => [v]), 10 | skip(before: (separator ?? char(',')).trim()).star(), 11 | ].toSequenceParser().map>((v) => v.expand((e) => e).toList()); 12 | 13 | /// Same in parenthesis. 14 | Parser inParens() => skip(before: char('('), after: char(')')); 15 | 16 | /// Same in brackets. 17 | Parser inBrackets() => skip(before: char('['), after: char(']')); 18 | 19 | Parser tryMap(T Function(R r) mapper) => TryMapParser(this, mapper); 20 | } 21 | 22 | extension ParserListStringExt on Parser> { 23 | Parser join([String separator = '']) => map((v) => v.join(separator)); 24 | } 25 | 26 | class TryMapParser extends MapParser { 27 | TryMapParser(super.delegate, super.callback); 28 | 29 | @override 30 | Result parseOn(Context context) { 31 | try { 32 | return super.parseOn(context); 33 | } on Exception catch (e) { 34 | return context.failure(e.toString()); 35 | } 36 | } 37 | 38 | @override 39 | TryMapParser copy() => TryMapParser(delegate, callback); 40 | } 41 | -------------------------------------------------------------------------------- /test/cases/standard/expressions_boolean.json: -------------------------------------------------------------------------------- 1 | { 2 | "tests": [ 3 | { 4 | "selector": "$[?@ > count($)]", 5 | "document": [-100, 0, 1, 2, 3, 3.14, 4, "4", "1", true, [], {}], 6 | "result": [2, 3, 3.14, 4] 7 | }, { 8 | "selector": "$[?((@ > 2) || (@ < -0.14))]", 9 | "document": [-100, 0, 1, 2, 3, 3.14, 4, "4", "1", true, [], {}], 10 | "result": [-100, 3, 3.14, 4] 11 | }, { 12 | "selector": "$[?((@ < 2) && @)]", 13 | "document": [-100, 0, 1, 2, 3, 3.14, 4, "4", "1", true, [], {}], 14 | "result": [-100, 0, 1] 15 | }, { 16 | "selector": "$[?@ > 2.5 || @ >= '1']", 17 | "document": [-100, 0, 1, 2, 3, 3.14, 4, "4", "1", "foo", true, [], {}, false], 18 | "result": [3, 3.14, 4, "4", "1", "foo"] 19 | }, { 20 | "selector": "$[?1 < 2]", 21 | "document": [-100, 0, 1, 2, 3, 3.14, 4, "4", "1", "foo", true, [], {}, false], 22 | "result": [-100, 0, 1, 2, 3, 3.14, 4, "4", "1", "foo", true, [], {}, false] 23 | }, { 24 | "name": "all that exists", 25 | "selector": "$[?@]", 26 | "document": [-42, -2.718, 0, 3.14, 4, "", "4", "0", "foo", true, [], {}, false], 27 | "result": [-42, -2.718, 0, 3.14, 4, "", "4", "0", "foo", true, [], {}, false] 28 | }, { 29 | "name": "none that exists", 30 | "selector": "$[?!@]", 31 | "document": [-42, -2.718, 0, 3.14, 4, "", "4", "0", "foo", true, [], {}, false], 32 | "result": [] 33 | }, { 34 | "name": "expression within expression", 35 | "selector": "$.a[?@>count($.b[?@>5])]", 36 | "document": {"a":[1, 2, 3, 4], "b": [3, 4, 5, 6, 7]}, 37 | "result": [3, 4] 38 | } 39 | ] 40 | } -------------------------------------------------------------------------------- /lib/src/grammar/compare.dart: -------------------------------------------------------------------------------- 1 | import 'package:maybe_just_nothing/maybe_just_nothing.dart'; 2 | import 'package:petitparser/petitparser.dart'; 3 | 4 | /// True if [a] equals [b]. 5 | bool _eq(Maybe a, Maybe b) => 6 | a.merge(b, _eqRaw).orGet(() => a is Nothing && b is Nothing); 7 | 8 | /// Deep equality of primitives, lists, maps. 9 | bool _eqRaw(dynamic a, dynamic b) => 10 | (a == b) || 11 | (a is List && 12 | b is List && 13 | a.length == b.length && 14 | List.generate(a.length, (i) => _eqRaw(a[i], b[i])).every((i) => i)) || 15 | (a is Map && 16 | b is Map && 17 | a.keys.length == b.keys.length && 18 | a.keys.every((k) => b.containsKey(k) && _eqRaw(a[k], b[k]))); 19 | 20 | /// True if [a] is greater or equal to [b]. 21 | bool _ge(Maybe a, Maybe b) => _gt(a, b) || _eq(a, b); 22 | 23 | /// True if [a] is strictly greater than [b]. 24 | bool _gt(Maybe a, Maybe b) => _lt(b, a); 25 | 26 | /// True if [a] is less or equal to [b]. 27 | bool _le(Maybe a, Maybe b) => _lt(a, b) || _eq(a, b); 28 | 29 | /// True if [a] is strictly less than [b]. 30 | bool _lt(Maybe a, Maybe b) => a 31 | .merge( 32 | b, 33 | (x, y) => 34 | (x is num && y is num && x < y) || 35 | (x is String && y is String && x.compareTo(y) < 0), 36 | ) 37 | .or(false); 38 | 39 | /// True if [a] is not equal to [b]. 40 | bool _ne(Maybe a, Maybe b) => !_eq(a, b); 41 | 42 | const _operations = { 43 | '==': _eq, 44 | '!=': _ne, 45 | '<=': _le, 46 | '>=': _ge, 47 | '<': _lt, 48 | '>': _gt, 49 | }; 50 | 51 | bool compare(String op, Maybe a, Maybe b) => 52 | (_operations[op] ?? (throw StateError('Invalid operation "$op"')))(a, b); 53 | 54 | final cmpOperator = _operations.keys.map(string).toChoiceParser().trim(); 55 | -------------------------------------------------------------------------------- /lib/src/json_path_parser.dart: -------------------------------------------------------------------------------- 1 | import 'package:json_path/src/expression/expression.dart'; 2 | import 'package:json_path/src/expression/nodes.dart'; 3 | import 'package:json_path/src/fun/fun.dart'; 4 | import 'package:json_path/src/fun/fun_factory.dart'; 5 | import 'package:json_path/src/fun/standard/count.dart'; 6 | import 'package:json_path/src/fun/standard/length.dart'; 7 | import 'package:json_path/src/fun/standard/match.dart'; 8 | import 'package:json_path/src/fun/standard/search.dart'; 9 | import 'package:json_path/src/fun/standard/value.dart'; 10 | import 'package:json_path/src/grammar/json_path.dart'; 11 | import 'package:json_path/src/json_path.dart'; 12 | import 'package:json_path/src/json_path_internal.dart'; 13 | import 'package:petitparser/petitparser.dart'; 14 | 15 | /// A customizable JSONPath parser. 16 | class JsonPathParser { 17 | /// Creates an instance of the parser. 18 | factory JsonPathParser({Iterable functions = const []}) => 19 | functions.isEmpty ? _standard : JsonPathParser._(functions); 20 | 21 | JsonPathParser._(Iterable functions) 22 | : _parser = JsonPathGrammarDefinition( 23 | FunFactory(_stdFun.followedBy(functions)), 24 | ).build(); 25 | 26 | /// The standard instance is pre-cached to speed up parsing when only 27 | /// the standard built-in functions are used. 28 | static final _standard = JsonPathParser._(_stdFun); 29 | 30 | /// Standard functions 31 | static const _stdFun = [Count(), Length(), Match(), Search(), Value()]; 32 | 33 | final Parser> _parser; 34 | 35 | /// Parses the JSONPath from s string [expression]. 36 | /// Returns an instance of [JsonPath] or throws a [FormatException]. 37 | JsonPath parse(String expression) => 38 | JsonPathInternal(expression, _parser.parse(expression).value.call); 39 | } 40 | -------------------------------------------------------------------------------- /lib/src/fun/fun_validator.dart: -------------------------------------------------------------------------------- 1 | import 'package:json_path/src/expression/nodes.dart'; 2 | import 'package:json_path/src/fun/fun.dart'; 3 | import 'package:json_path/src/grammar/fun_name.dart'; 4 | import 'package:maybe_just_nothing/maybe_just_nothing.dart'; 5 | import 'package:petitparser/petitparser.dart'; 6 | 7 | class FunValidator { 8 | /// Validates the function and returns all issues found. 9 | /// An empty return means a successful validation. 10 | Iterable errors(Fun f) sync* { 11 | if (!funName.allMatches(f.name).contains(f.name)) { 12 | yield 'Invalid function name ${f.name}'; 13 | } 14 | if (f is Fun1) { 15 | if (f is! Fun1 && 16 | f is! Fun1 && 17 | f is! Fun1) { 18 | yield 'Invalid return type in function ${f.name}'; 19 | } 20 | if (f is! Fun1 && 21 | f is! Fun1 && 22 | f is! Fun1) { 23 | yield 'Invalid type of the first argument in function ${f.name}'; 24 | } 25 | } else if (f is Fun2) { 26 | if (f is! Fun2 && 27 | f is! Fun2 && 28 | f is! Fun2) { 29 | yield 'Invalid return type in function ${f.name}'; 30 | } 31 | if (f is! Fun2 && 32 | f is! Fun2 && 33 | f is! Fun2) { 34 | yield 'Invalid type of the first argument in function ${f.name}'; 35 | } 36 | if (f is! Fun2 && 37 | f is! Fun2 && 38 | f is! Fun2) { 39 | yield 'Invalid type of the second argument in function ${f.name}'; 40 | } 41 | } else { 42 | yield 'Unexpected function type ${f.runtimeType}'; 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to jessie 2 | 3 | The jessie project team welcomes contributions from the community. 4 | 5 | ## Contribution Flow 6 | 7 | - Fork and clone the repository 8 | - Create a topic branch from where you want to base your work 9 | - Make commits of logical units 10 | - Make sure the tests pass 11 | - Push your changes to a topic branch in your fork of the repository 12 | - Submit a pull request 13 | 14 | Example: 15 | 16 | ``` shell 17 | git remote add upstream git@github.com:f3ath/jessie.git 18 | git checkout -b my-new-feature main 19 | git commit -a 20 | git push origin my-new-feature 21 | ``` 22 | 23 | ### Running the Test Suite 24 | 25 | The test suite is included as a submodule. To run the tests, you need to initialize the submodule: 26 | 27 | ``` shell 28 | git submodule update --init 29 | ``` 30 | 31 | Then you can run the tests: 32 | 33 | ``` shell 34 | dart test 35 | ``` 36 | 37 | ### Staying In Sync With Upstream 38 | 39 | When your branch gets out of sync with the jsonpath-standard/jessie/main branch, use the following to update: 40 | 41 | ``` shell 42 | git checkout my-new-feature 43 | git fetch -a 44 | git pull --rebase upstream main 45 | git push --force-with-lease origin my-new-feature 46 | ``` 47 | 48 | ### Updating pull requests 49 | 50 | If your PR fails to pass CI or needs changes based on code review, you'll most likely want to squash these changes into 51 | existing commits. 52 | 53 | If your pull request contains a single commit or your changes are related to the most recent commit, you can simply 54 | amend the commit. 55 | 56 | ``` shell 57 | git add . 58 | git commit --amend 59 | git push --force-with-lease origin my-new-feature 60 | ``` 61 | 62 | If you need to squash changes into an earlier commit, you can use: 63 | 64 | ``` shell 65 | git add . 66 | git commit --fixup 67 | git rebase -i --autosquash main 68 | git push --force-with-lease origin my-new-feature 69 | ``` 70 | -------------------------------------------------------------------------------- /test/fun_factory_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:json_path/src/fun/fun.dart'; 2 | import 'package:json_path/src/fun/fun_factory.dart'; 3 | import 'package:maybe_just_nothing/maybe_just_nothing.dart'; 4 | import 'package:test/test.dart'; 5 | 6 | void main() { 7 | group('FunFactory', () { 8 | test('throws on incorrect fun type', () { 9 | expect(() => FunFactory([ForbiddenFun()]), throwsArgumentError); 10 | }); 11 | test('throws on incorrect fun name', () { 12 | expect(() => FunFactory([BadNameFun()]), throwsArgumentError); 13 | }); 14 | test('throws on invalid return type', () { 15 | expect(() => FunFactory([BadReturnType1Fun()]), throwsArgumentError); 16 | expect(() => FunFactory([BadReturnType2Fun()]), throwsArgumentError); 17 | }); 18 | 19 | test('throws on invalid arg type', () { 20 | expect(() => FunFactory([BadFirstArg1Fun()]), throwsArgumentError); 21 | expect(() => FunFactory([BadFirstArg2Fun()]), throwsArgumentError); 22 | expect(() => FunFactory([BadSecondArg2Fun()]), throwsArgumentError); 23 | }); 24 | }); 25 | } 26 | 27 | class ForbiddenFun implements Fun { 28 | @override 29 | final name = 'oops'; 30 | } 31 | 32 | class BadNameFun implements Fun1 { 33 | @override 34 | final name = 'Foo'; 35 | 36 | @override 37 | bool call(Maybe a) => true; 38 | } 39 | 40 | class BadReturnType1Fun implements Fun1 { 41 | @override 42 | final name = 'bad'; 43 | 44 | @override 45 | int call(bool a) => 42; 46 | } 47 | 48 | class BadFirstArg1Fun implements Fun1 { 49 | @override 50 | final name = 'bad'; 51 | 52 | @override 53 | bool call(int a) => true; 54 | } 55 | 56 | class BadReturnType2Fun implements Fun2 { 57 | @override 58 | final name = 'bad'; 59 | 60 | @override 61 | int call(bool a, bool b) => 42; 62 | } 63 | 64 | class BadFirstArg2Fun implements Fun2 { 65 | @override 66 | final name = 'bad'; 67 | 68 | @override 69 | bool call(int a, bool b) => true; 70 | } 71 | 72 | class BadSecondArg2Fun implements Fun2 { 73 | @override 74 | final name = 'bad'; 75 | 76 | @override 77 | bool call(bool a, int b) => true; 78 | } 79 | -------------------------------------------------------------------------------- /test/cases/standard/fun_search.json: -------------------------------------------------------------------------------- 1 | { 2 | "tests": [ 3 | { 4 | "selector": "$[?search(@, 'a')]", 5 | "document": ["abc", "bcd", "bab", "bba", "a", true, [], {}], 6 | "result": ["abc", "bab", "bba", "a"] 7 | }, 8 | { 9 | "name": "filter, search function, unicode char class, uppercase", 10 | "selector": "$[?search(@, '\\\\p{Lu}')]", 11 | "document": ["ж", "Ж", "1", "жЖ", true, [], {}], 12 | "result": ["Ж", "жЖ"] 13 | }, 14 | { 15 | "name": "filter, search function, unicode char class negated, uppercase", 16 | "selector": "$[?search(@, '\\\\P{Lu}')]", 17 | "document": ["ж", "Ж", "1", true, [], {}], 18 | "result": ["ж", "1"] 19 | }, 20 | { 21 | "name": "filter, search function, unicode, surrogate pair", 22 | "selector": "$[?search(@, 'a.b')]", 23 | "document": ["a\uD800\uDD01bc", "abc", "1", true, [], {}], 24 | "result": ["a\uD800\uDD01bc"] 25 | }, 26 | { 27 | "selector": "$.foo[?search(@, 'a')]", 28 | "document": {"foo": ["abc", "bcd", "bab", "bba", "a", true, [], {}]}, 29 | "result": ["abc", "bab", "bba", "a"] 30 | }, 31 | { 32 | "selector": "$[?search('bab', 'a')]", 33 | "document": ["abc", "bcd", "bab", "bba", "a", true, [], {}], 34 | "result": ["abc", "bcd", "bab", "bba", "a", true, [], {}] 35 | }, 36 | { 37 | "selector": "$.result[?search(@, $.regex)]", 38 | "document": {"regex": "b.?b", "result": ["abc", "bcd", "bab", "bba", "a", true, [], {}]}, 39 | "result": ["bab", "bba"] 40 | }, 41 | { 42 | "selector": "$.result[?search(@, $.regex)]", 43 | "document": {"regex": "invalid][$^", "result": ["abc", "bcd", "bab", "bba", "a", true, [], {}]}, 44 | "result": [] 45 | }, 46 | { 47 | "name": "dot matcher on \\u2028", 48 | "selector": "$[?search(@, '.')]", 49 | "document": ["\u2028", "\r\u2028\n", "\r", "\n", true, [], {}], 50 | "result": ["\u2028", "\r\u2028\n"] 51 | }, 52 | { 53 | "name": "dot matcher on \\u2029", 54 | "selector": "$[?search(@, '.')]", 55 | "document": ["\u2029", "\r\u2029\n", "\r", "\n", true, [], {}], 56 | "result": ["\u2029", "\r\u2029\n"] 57 | } 58 | ] 59 | } -------------------------------------------------------------------------------- /example/example.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | import 'package:json_path/json_path.dart'; 4 | 5 | void main() { 6 | final document = jsonDecode(''' 7 | { 8 | "store": { 9 | "book": [ 10 | { 11 | "category": "reference", 12 | "author": "Nigel Rees", 13 | "title": "Sayings of the Century", 14 | "price": 8.95 15 | }, 16 | { 17 | "category": "fiction", 18 | "author": "Evelyn Waugh", 19 | "title": "Sword of Honour", 20 | "price": 12.99 21 | }, 22 | { 23 | "category": "fiction", 24 | "author": "Herman Melville", 25 | "title": "Moby Dick", 26 | "isbn": "0-553-21311-3", 27 | "price": 8.99 28 | }, 29 | { 30 | "category": "fiction", 31 | "author": "J. R. R. Tolkien", 32 | "title": "The Lord of the Rings", 33 | "isbn": "0-395-19395-8", 34 | "price": 22.99 35 | } 36 | ], 37 | "bicycle": { 38 | "color": "red", 39 | "price": 19.95 40 | } 41 | } 42 | } 43 | '''); 44 | 45 | print('All prices in the store, by JSONPath:'); 46 | JsonPath(r'$..price') 47 | .read(document) 48 | .map((match) => '${match.path}:\t${match.value}') 49 | .forEach(print); 50 | 51 | print('\nSame, by JSON Pointer:'); 52 | JsonPath(r'$..price') 53 | .read(document) 54 | .map((match) => '${match.pointer}:\t${match.value}') 55 | .forEach(print); 56 | 57 | print('\nSame, but just the values:'); 58 | JsonPath(r'$..price').readValues(document).forEach(print); 59 | 60 | print('\nBooks under 10:'); 61 | JsonPath( 62 | r'$.store.book[?@.price < 10].title', 63 | ).readValues(document).forEach(print); 64 | 65 | print('\nBooks with ISBN:'); 66 | JsonPath(r'$.store.book[?@.isbn].title').readValues(document).forEach(print); 67 | 68 | print('\nBooks under 10 with ISBN:'); 69 | JsonPath( 70 | r'$.store.book[?@.price < 10 && @.isbn].title', 71 | ).readValues(document).forEach(print); 72 | 73 | print('\nBooks with "the" in the title:'); 74 | JsonPath( 75 | r'$.store.book[?search(@.title, "the")].title', 76 | ).readValues(document).forEach(print); 77 | 78 | print('\nBooks with the same category as the last one:'); 79 | JsonPath( 80 | r'$.store.book[?@.category == $.store.book[-1].category].title', 81 | ).readValues(document).forEach(print); 82 | } 83 | -------------------------------------------------------------------------------- /test/cases/standard/expressions_ordering.json: -------------------------------------------------------------------------------- 1 | { 2 | "tests": [ 3 | { 4 | "selector": "$[?(@ > 2)]", 5 | "document": [-100, 0, 1, 2, 3, 3.14, 4, "1", "2", "3", true, false, [], {}], 6 | "result": [3, 3.14, 4] 7 | }, { 8 | "selector": "$[?(@ > '2')]", 9 | "document": [-100, 0, 1, 2, 3, 3.14, 4, "1", "2", "3", true, false, [], {}], 10 | "result": ["3"] 11 | }, { 12 | "selector": "$[?(@ >= 2)]", 13 | "document": [-100, 0, 1, 2, 3, 3.14, 4, "1", "2", "3", true, false, [], {}], 14 | "result": [2, 3, 3.14, 4] 15 | }, { 16 | "selector": "$[?(@ >= '2')]", 17 | "document": [-100, 0, 1, 2, 3, 3.14, 4, "1", "2", "3", true, false, [], {}], 18 | "result": ["2", "3"] 19 | }, { 20 | "selector": "$[?(@ < 2)]", 21 | "document": [-100, 0, 1, 2, 3, 3.14, 4, "4", "1", true, [], {}], 22 | "result": [-100, 0, 1] 23 | }, { 24 | "selector": "$[?(@ < '2')]", 25 | "document": [-100, 0, 1, 2, 3, 3.14, 4, "4", "1", true, [], {}], 26 | "result": ["1"] 27 | }, { 28 | "selector": "$[?(@ <= 2)]", 29 | "document": [-100, 0, 1, 2, 3, 3.14, 4, "1", "2", "3", true, false, [], {}], 30 | "result": [-100, 0, 1, 2] 31 | }, { 32 | "selector": "$[?(@ <= '2')]", 33 | "document": [-100, 0, 1, 2, 3, 3.14, 4, "1", "2", "3", true, false, [], {}], 34 | "result": ["1", "2"] 35 | }, { 36 | "selector": "$[?(@ <= 'bar')]", 37 | "document": ["", "foo", "bar", "baz"], 38 | "result": ["", "bar"] 39 | }, { 40 | "selector": "$[?(@ >= 'bar')]", 41 | "document": ["", "foo", "bar", "baz"], 42 | "result": ["foo", "bar", "baz"] 43 | }, 44 | { 45 | "name": "props comparison", 46 | "selector": "$[?(@.foo < @.bar)]", 47 | "document": [{"foo": 1, "bar": 2}, {"foo": 42, "bar": 42}, {"foo": 1, "bro": 1}, {}], 48 | "result": [{"foo": 1, "bar": 2}] 49 | }, 50 | { 51 | "name": "props comparison, boolean OR", 52 | "selector": "$[?((@.foo < 2) || (@.foo > 2))]", 53 | "document": [{"foo": 1}, {"foo": 2}, {"foo": 3}, {}], 54 | "result": [{"foo": 1}, {"foo": 3}] 55 | }, 56 | { 57 | "name": "props comparison, boolean AND", 58 | "selector": "$[?(@.key > 42 && @.key < 44)]", 59 | "document": [{"key": 42}, {"key": 43}, {"key": 44}], 60 | "result": [{"key": 43}] 61 | } 62 | ] 63 | } -------------------------------------------------------------------------------- /test/cases/standard/normalized_path.json: -------------------------------------------------------------------------------- 1 | { 2 | "tests": [ 3 | { 4 | "name": "escaped characters", 5 | "selector": "$[*]", 6 | "document": { 7 | "\u0000": "NUL", 8 | "\u0001": "SOH", 9 | "\u0002": "STX", 10 | "\u0003": "ETX", 11 | "\u0004": "EOT", 12 | "\u0005": "ENQ", 13 | "\u0006": "ACK", 14 | "\u0007": "nul", 15 | "\u0008": "BS, backspace", 16 | "\u0009": "HT, tab", 17 | "\u000A": "LF, new line", 18 | "\u000B": "VT", 19 | "\u000C": "FF, form feed", 20 | "\u000D": "CR, carriage return", 21 | "\u000E": "SO", 22 | "\u000F": "SI", 23 | "\u001F": "US", 24 | "'": "single quote/apostrophe", 25 | "\\": "backslash" 26 | }, 27 | "result": [ 28 | "NUL", 29 | "SOH", 30 | "STX", 31 | "ETX", 32 | "EOT", 33 | "ENQ", 34 | "ACK", 35 | "nul", 36 | "BS, backspace", 37 | "HT, tab", 38 | "LF, new line", 39 | "VT", 40 | "FF, form feed", 41 | "CR, carriage return", 42 | "SO", 43 | "SI", 44 | "US", 45 | "single quote/apostrophe", 46 | "backslash" 47 | ], 48 | "result_paths": [ 49 | "$['\\u0000']", 50 | "$['\\u0001']", 51 | "$['\\u0002']", 52 | "$['\\u0003']", 53 | "$['\\u0004']", 54 | "$['\\u0005']", 55 | "$['\\u0006']", 56 | "$['\\u0007']", 57 | "$['\\b']", 58 | "$['\\t']", 59 | "$['\\n']", 60 | "$['\\u000b']", 61 | "$['\\f']", 62 | "$['\\r']", 63 | "$['\\u000e']", 64 | "$['\\u000f']", 65 | "$['\\u001f']", 66 | "$['\\'']", 67 | "$['\\\\']" 68 | ] 69 | }, 70 | { 71 | "name": "unescaped characters", 72 | "selector": "$[*]", 73 | "document": { 74 | "\"": "double quote", 75 | "/": "slash", 76 | "[]": "[]", 77 | "\ud83d\ude00": "smiley face", 78 | "\u10FFFF": "0x10FFFF" 79 | }, 80 | "result": [ 81 | "double quote", 82 | "slash", 83 | "[]", 84 | "smiley face", 85 | "0x10FFFF" 86 | ], 87 | "result_paths": [ 88 | "$['\"']", 89 | "$['/']", 90 | "$['[]']", 91 | "$['\uD83D\uDE00']", 92 | "$['\u10FFFF']" 93 | ] 94 | } 95 | ] 96 | } -------------------------------------------------------------------------------- /test/cases/standard/whitespace.json: -------------------------------------------------------------------------------- 1 | { 2 | "tests": [ 3 | { 4 | "name": "spaces in a relative singular selector", 5 | "selector" : "$[?length(@ .a .b) == 3]", 6 | "document" : [ {"a": {"b": "foo"}}, {} ], 7 | "result": [ {"a": {"b": "foo"}} ] 8 | }, 9 | { 10 | "name": "newlines in a relative singular selector", 11 | "selector" : "$[?length(@\n.a\n.b) == 3]", 12 | "document" : [ {"a": {"b": "foo"}}, {} ], 13 | "result": [ {"a": {"b": "foo"}} ] 14 | }, 15 | { 16 | "name": "tabs in a relative singular selector", 17 | "selector" : "$[?length(@\t.a\t.b) == 3]", 18 | "document" : [ {"a": {"b": "foo"}}, {} ], 19 | "result": [ {"a": {"b": "foo"}} ] 20 | }, 21 | { 22 | "name": "returns in a relative singular selector", 23 | "selector" : "$[?length(@\r.a\r.b) == 3]", 24 | "document" : [ {"a": {"b": "foo"}}, {} ], 25 | "result": [ {"a": {"b": "foo"}} ] 26 | }, 27 | { 28 | "name": "spaces in an absolute singular selector ", 29 | "selector" : "$..[?length(@)==length($ [0] .a)]", 30 | "document" : [ {"a": "foo"}, {} ], 31 | "result": [ "foo" ] 32 | }, 33 | { 34 | "name": "newlines in an absolute singular selector ", 35 | "selector" : "$..[?length(@)==length($\n[0]\n.a)]", 36 | "document" : [ {"a": "foo"}, {} ], 37 | "result": [ "foo" ] 38 | }, 39 | { 40 | "name": "tabs in an absolute singular selector ", 41 | "selector" : "$..[?length(@)==length($\t[0]\t.a)]", 42 | "document" : [ {"a": "foo"}, {} ], 43 | "result": [ "foo" ] 44 | }, 45 | { 46 | "name": "returns in an absolute singular selector", 47 | "selector" : "$..[?length(@)==length($\r[0]\r.a)]", 48 | "document" : [ {"a": "foo"}, {} ], 49 | "result": [ "foo" ] 50 | }, 51 | { 52 | "name": "space between ? and function", 53 | "selector" : "$..[? length(@)==1]", 54 | "document" : [ {"a": "foo"}, {} ], 55 | "result": [ {"a": "foo"} ] 56 | }, 57 | { 58 | "name": "tab between ? and function", 59 | "selector" : "$..[?\tlength(@)==1]", 60 | "document" : [ {"a": "foo"}, {} ], 61 | "result": [ {"a": "foo"} ] 62 | }, 63 | { 64 | "name": "\\n between ? and function", 65 | "selector" : "$..[ ?\nlength(@) == 1 ]", 66 | "document" : [ {"a": "foo"}, {} ], 67 | "result": [ {"a": "foo"} ] 68 | }, 69 | { 70 | "name": "\\r between ? and function", 71 | "selector" : "$..[ ?\rlength(@) == 1 ]", 72 | "document" : [ {"a": "foo"}, {} ], 73 | "result": [ {"a": "foo"} ] 74 | } 75 | ] 76 | } -------------------------------------------------------------------------------- /lib/src/node.dart: -------------------------------------------------------------------------------- 1 | import 'package:json_path/src/grammar/slice_indices.dart'; 2 | 3 | /// A JSON document node. 4 | class Node { 5 | /// Creates an instance of the root node of the JSON document [value]. 6 | Node(this.value) : parent = null, key = null, index = null; 7 | 8 | /// Creates an instance of a child node. 9 | Node._(this.value, this.parent, {this.key, this.index}); 10 | 11 | /// The node value. 12 | final T value; 13 | 14 | /// The parent node. 15 | final Node? parent; 16 | 17 | /// The root node of the entire document. 18 | Node get root => parent?.root ?? this; 19 | 20 | /// For a node which is an object child, this is its [key] in the [parent] 21 | /// node. 22 | final String? key; 23 | 24 | /// For a node which is an element of an array, this is its [index] 25 | /// in the [parent] node. 26 | final int? index; 27 | 28 | /// For a node whose value is an array, returns the slice of 29 | /// its children. 30 | Iterable? slice({int? start, int? stop, int? step}) { 31 | final v = value; 32 | if (v is List) { 33 | return sliceIndices( 34 | v.length, 35 | start, 36 | stop, 37 | step ?? 1, 38 | ).map((index) => _element(v, index)); 39 | } 40 | return null; 41 | } 42 | 43 | /// All direct children of the node. 44 | Iterable get children sync* { 45 | final v = value; 46 | if (v is Map) yield* v.keys.map((key) => _child(v, key)); 47 | if (v is List) yield* v.asMap().keys.map((index) => _element(v, index)); 48 | } 49 | 50 | /// Returns the JSON array element at the [offset] if it exists, 51 | /// otherwise returns null. Negative offsets are supported. 52 | Node? element(int offset) { 53 | final v = value; 54 | if (v is List) { 55 | final index = offset < 0 ? v.length + offset : offset; 56 | if (index >= 0 && index < v.length) return _element(v, index); 57 | } 58 | return null; 59 | } 60 | 61 | /// Returns the JSON object child at the [key] if it exists, 62 | /// otherwise returns null. 63 | Node? child(String key) { 64 | final v = value; 65 | if (v is Map && v.containsKey(key)) return _child(v, key); 66 | return null; 67 | } 68 | 69 | Node _element(List list, int index) => 70 | Node._(list[index], this, index: index); 71 | 72 | Node _child(Map map, String key) => Node._(map[key], this, key: key); 73 | 74 | @override 75 | bool operator ==(Object other) => 76 | other is Node && 77 | other.value == value && 78 | other.parent == parent && 79 | other.index == index && 80 | other.key == key; 81 | 82 | @override 83 | int get hashCode => value.hashCode; 84 | 85 | @override 86 | String toString() => 'Node($value)'; 87 | } 88 | -------------------------------------------------------------------------------- /lib/src/grammar/strings.dart: -------------------------------------------------------------------------------- 1 | import 'package:json_path/src/grammar/child_selector.dart'; 2 | import 'package:json_path/src/grammar/parser_ext.dart'; 3 | import 'package:petitparser/petitparser.dart'; 4 | 5 | final _escape = char(r'\'); 6 | final _doubleQuote = char('"'); 7 | 8 | final _singleQuote = char("'"); 9 | 10 | final _escapedSlash = string(r'\/').value(r'/'); 11 | final _escapedBackSlash = string(r'\\').value(r'\'); 12 | 13 | final _escapedBackspace = string(r'\b').value('\b'); 14 | final _escapedFormFeed = string(r'\f').value('\f'); 15 | 16 | final _escapedNewLine = string(r'\n').value('\n'); 17 | final _escapedReturn = string(r'\r').value('\r'); 18 | final _escapedTab = string(r'\t').value('\t'); 19 | final _escapedControl = [ 20 | _escapedSlash, 21 | _escapedBackSlash, 22 | _escapedBackspace, 23 | _escapedFormFeed, 24 | _escapedNewLine, 25 | _escapedReturn, 26 | _escapedTab, 27 | ].toChoiceParser(); 28 | 29 | // The highest unicode character 30 | final _unicodeBoundary = String.fromCharCode(0xFFFF); 31 | 32 | // Exclude double quote '"' and back slash '\' 33 | final _doubleUnescaped = [ 34 | range(' ', '!'), 35 | range('#', '['), 36 | range(']', _unicodeBoundary), 37 | ].toChoiceParser(); 38 | 39 | final _hexDigit = anyOf('0123456789ABCDEFabcdef'); 40 | 41 | final _unicodeSymbol = (string(r'\u') & _hexDigit.timesString(4)).map( 42 | (value) => String.fromCharCode(int.parse(value.last, radix: 16)), 43 | ); 44 | 45 | final _escapedDoubleQuote = (_escape & _doubleQuote).map((_) => '"'); 46 | 47 | final _doubleInner = [ 48 | _doubleUnescaped, 49 | _escapedDoubleQuote, 50 | _escapedControl, 51 | _unicodeSymbol, 52 | ].toChoiceParser().star().join(); 53 | 54 | // Exclude single quote "'" and back slash "\" 55 | final _singleUnescaped = [ 56 | range(' ', '&'), 57 | range('(', '['), 58 | range(']', _unicodeBoundary), 59 | ].toChoiceParser(); 60 | 61 | final _escapedSingleQuote = (_escape & _singleQuote).map((_) => "'"); 62 | 63 | final _singleInner = [ 64 | _singleUnescaped, 65 | _escapedSingleQuote, 66 | _escapedControl, 67 | _unicodeSymbol, 68 | ].toChoiceParser().star().join(); 69 | 70 | final _doubleQuotedString = _doubleInner.skip( 71 | before: _doubleQuote, 72 | after: _doubleQuote, 73 | ); 74 | 75 | final _singleQuotedString = _singleInner.skip( 76 | before: _singleQuote, 77 | after: _singleQuote, 78 | ); 79 | 80 | final _nameFirst = 81 | (char('_') | letter() | range(String.fromCharCode(0x80), _unicodeBoundary)) 82 | .plus() 83 | .flatten(message: 'a correct member name expected'); 84 | 85 | final _nameChar = digit() | _nameFirst; 86 | 87 | final quotedString = (_singleQuotedString | _doubleQuotedString).cast(); 88 | 89 | final memberNameShorthand = (_nameFirst & _nameChar.star()) 90 | .flatten(message: 'a member name shorthand expected') 91 | .map(childSelector); 92 | -------------------------------------------------------------------------------- /test/cases/standard/escaping.json: -------------------------------------------------------------------------------- 1 | { 2 | "tests": [ 3 | { 4 | "name": "escaping, single quotes, forward slash unescaped", 5 | "selector": "$['A/B']", 6 | "document": {"A/B": 42}, 7 | "result": [42] 8 | }, { 9 | "name": "escaping, single quotes, forward slash escaped", 10 | "selector": "$['A\\/B']", 11 | "document": {"A/B": 42}, 12 | "result": [42] 13 | }, { 14 | "name": "escaping, single quotes, \\b", 15 | "selector": "$['A\\bB']", 16 | "document": {"A\bB": 42}, 17 | "result": [42] 18 | }, { 19 | "name": "escaping, single quotes, \\", 20 | "selector": "$['A\\\\B']", 21 | "document": {"A\\B": 42}, 22 | "result": [42] 23 | }, { 24 | "name": "escaping, single quotes, \\t", 25 | "selector": "$['A\\tB']", 26 | "document": {"A\tB": 42}, 27 | "result": [42] 28 | }, { 29 | "name": "escaping, single quotes, \\r", 30 | "selector": "$['A\\rB']", 31 | "document": {"A\rB": 42}, 32 | "result": [42] 33 | }, { 34 | "name": "escaping, single quotes, \\n", 35 | "selector": "$['A\\nB']", 36 | "document": {"A\nB": 42}, 37 | "result": [42] 38 | }, { 39 | "name": "escaping, single quotes, \\f", 40 | "selector": "$['A\\fB']", 41 | "document": {"A\fB": 42}, 42 | "result": [42] 43 | }, { 44 | "name": "escaping, single quotes, \"", 45 | "selector": "$['A\"B']", 46 | "document": {"A\"B": 42}, 47 | "result": [42] 48 | }, { 49 | "name": "escaping, single quotes, '", 50 | "selector": "$['A\\'B']", 51 | "document": {"A'B": 42}, 52 | "result": [42] 53 | }, { 54 | "name": "escaping, double quotes, forward slash unescaped", 55 | "selector": "$[\"A/B\"]", 56 | "document": {"A/B": 42}, 57 | "result": [42] 58 | }, { 59 | "name": "escaping, double quotes, forward slash escaped", 60 | "selector": "$[\"A\\/B\"]", 61 | "document": {"A/B": 42}, 62 | "result": [42] 63 | }, { 64 | "name": "escaping, double quotes, \\b", 65 | "selector": "$[\"A\\bB\"]", 66 | "document": {"A\bB": 42}, 67 | "result": [42] 68 | }, { 69 | "name": "escaping, double quotes, \\", 70 | "selector": "$[\"A\\\\B\"]", 71 | "document": {"A\\B": 42}, 72 | "result": [42] 73 | }, { 74 | "name": "escaping, double quotes, \\t", 75 | "selector": "$[\"A\\tB\"]", 76 | "document": {"A\tB": 42}, 77 | "result": [42] 78 | }, { 79 | "name": "escaping, double quotes, \\r", 80 | "selector": "$[\"A\\rB\"]", 81 | "document": {"A\rB": 42}, 82 | "result": [42] 83 | }, { 84 | "name": "escaping, double quotes, \\n", 85 | "selector": "$[\"A\\nB\"]", 86 | "document": {"A\nB": 42}, 87 | "result": [42] 88 | }, { 89 | "name": "escaping, double quotes, \\f", 90 | "selector": "$[\"A\\fB\"]", 91 | "document": {"A\fB": 42}, 92 | "result": [42] 93 | }, { 94 | "name": "escaping, double quotes, \"", 95 | "selector": "$[\"A\\\"B\"]", 96 | "document": {"A\"B": 42}, 97 | "result": [42] 98 | }, { 99 | "name": "escaping, double quotes, '", 100 | "selector": "$[\"A'B\"]", 101 | "document": {"A'B": 42}, 102 | "result": [42] 103 | } 104 | ] 105 | } -------------------------------------------------------------------------------- /lib/src/fun/fun_factory.dart: -------------------------------------------------------------------------------- 1 | import 'package:json_path/src/expression/expression.dart'; 2 | import 'package:json_path/src/expression/nodes.dart'; 3 | import 'package:json_path/src/fun/fun.dart'; 4 | import 'package:json_path/src/fun/fun_call.dart'; 5 | import 'package:json_path/src/fun/fun_validator.dart'; 6 | import 'package:maybe_just_nothing/maybe_just_nothing.dart'; 7 | 8 | class FunFactory { 9 | FunFactory(Iterable functions) { 10 | final errors = functions.expand(_validator.errors); 11 | if (errors.isNotEmpty) { 12 | throw ArgumentError('Function validation errors: ${errors.join(', ')}'); 13 | } 14 | for (final f in functions) { 15 | if (f is Fun1) _fun1[f.name] = f; 16 | if (f is Fun2) _fun2[f.name] = f; 17 | } 18 | } 19 | 20 | static final _validator = FunValidator(); 21 | 22 | final _fun1 = {}; 23 | final _fun2 = {}; 24 | 25 | /// Returns a value-type function to use in comparable context. 26 | Expression value(FunCall call) => _any(call); 27 | 28 | /// Returns a logical-type function to use in logical context. 29 | Expression logical(FunCall call) => _any(call); 30 | 31 | /// Returns a nodes-type function. 32 | Expression nodes(FunCall call) => _any(call); 33 | 34 | /// Returns a function to use as an argument for another function. 35 | Expression _any(FunCall call) => switch (call.args) { 36 | [var a] => _any1(call.name, a), 37 | [var a, var b] => _any2(call.name, a, b), 38 | _ => throw Exception('Invalid number of args for ${call.name}()'), 39 | }; 40 | 41 | Expression _any1(String name, Expression a0) { 42 | final f = _getFun1(name); 43 | final cast0 = cast( 44 | a0, 45 | value: f is Fun1, 46 | logical: f is Fun1, 47 | node: f is Fun1, 48 | nodes: f is Fun1, 49 | ); 50 | return cast0.map(f.call); 51 | } 52 | 53 | Expression _any2( 54 | String name, 55 | Expression a0, 56 | Expression a1, 57 | ) { 58 | final f = _getFun2(name); 59 | final cast0 = cast( 60 | a0, 61 | value: f is Fun2, 62 | logical: f is Fun2, 63 | node: f is Fun2, 64 | nodes: f is Fun2, 65 | ); 66 | final cast1 = cast( 67 | a1, 68 | value: f is Fun2, 69 | logical: f is Fun2, 70 | node: f is Fun2, 71 | nodes: f is Fun2, 72 | ); 73 | return cast0.merge(cast1, f.call); 74 | } 75 | 76 | Fun1 _getFun1(String name) { 77 | final f = _fun1[name]; 78 | if (f is Fun1) return f; 79 | throw FormatException('Function "$name" of 1 argument is not found'); 80 | } 81 | 82 | Fun2 _getFun2(String name) { 83 | final f = _fun2[name]; 84 | if (f is Fun2) return f; 85 | throw FormatException('Function "$name" of 2 arguments is not found'); 86 | } 87 | 88 | static Expression cast( 89 | Expression arg, { 90 | required bool value, 91 | required bool logical, 92 | required bool node, 93 | required bool nodes, 94 | }) { 95 | if (value) { 96 | if (arg is Expression) return arg; 97 | if (arg is Expression) return arg.map((v) => v.asValue); 98 | } else if (logical) { 99 | if (arg is Expression) return arg; 100 | if (arg is Expression) return arg.map((v) => v.asLogical); 101 | } else if (node) { 102 | if (arg is Expression) return arg; 103 | } else if (nodes) { 104 | if (arg is Expression) return arg; 105 | } 106 | throw Exception('Arg type mismatch'); 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /test/helper.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | import 'dart:io'; 3 | 4 | import 'package:json_path/json_path.dart'; 5 | import 'package:path/path.dart' as path; 6 | import 'package:test/test.dart'; 7 | 8 | void runTestsInDirectory(String dirName, {JsonPathParser? parser}) { 9 | JsonPath jsonPath(String expression) => 10 | parser?.parse(expression) ?? JsonPath(expression); 11 | 12 | Directory(dirName) 13 | .listSync() 14 | .whereType() 15 | .where( 16 | (file) => 17 | file.path.endsWith('.json') && !file.path.endsWith('.schema.json'), 18 | ) 19 | .forEach((file) { 20 | group(path.basename(file.path), () { 21 | final cases = jsonDecode(file.readAsStringSync()); 22 | for (final Map t in cases['tests'] as List) { 23 | for (final key in t.keys) { 24 | if (!_knownKeys.contains(key)) { 25 | throw ArgumentError('Unknown key "$key"'); 26 | } 27 | } 28 | 29 | final String selector = t['selector']; 30 | final document = t['document']; 31 | final String? name = t['name']; 32 | final List? resultPaths = t['result_paths']; 33 | final List? resultsPaths = t['results_paths']; 34 | final List? pointers = t['result_pointers']; 35 | final String? skip = t['skip']; 36 | final List? result = t['result']; 37 | final List? results = t['results']; 38 | final bool? invalid = t['invalid_selector']; 39 | group(name ?? selector, () { 40 | if (result is List) { 41 | test( 42 | 'values', 43 | () => expect( 44 | jsonPath(selector).readValues(document), 45 | equals(result), 46 | ), 47 | ); 48 | } 49 | if (results is List) { 50 | test( 51 | 'any of values', 52 | () => expect( 53 | jsonPath(selector).readValues(document), 54 | anyOf(results), 55 | ), 56 | ); 57 | } 58 | if (resultPaths is List) { 59 | test( 60 | 'result_paths', 61 | () => expect( 62 | jsonPath( 63 | selector, 64 | ).read(document).map((e) => e.path).toList(), 65 | equals(resultPaths), 66 | ), 67 | ); 68 | } 69 | if (resultsPaths is List) { 70 | test( 71 | 'results_paths', 72 | () => expect( 73 | jsonPath( 74 | selector, 75 | ).read(document).map((e) => e.path).toList(), 76 | anyOf(resultsPaths), 77 | ), 78 | ); 79 | } 80 | if (pointers is List) { 81 | test( 82 | 'result_pointers', 83 | () => expect( 84 | jsonPath( 85 | selector, 86 | ).read(document).map((e) => e.pointer.toString()).toList(), 87 | equals(pointers), 88 | ), 89 | ); 90 | } 91 | if (invalid == true) { 92 | test( 93 | 'invalid', 94 | () => expect(() => jsonPath(selector), throwsFormatException), 95 | ); 96 | } 97 | if ((result ?? results ?? resultPaths ?? pointers ?? invalid) == 98 | null) { 99 | throw ArgumentError('No expectations found'); 100 | } 101 | }, skip: skip); 102 | } 103 | }); 104 | }); 105 | } 106 | 107 | const _knownKeys = { 108 | 'document', 109 | 'invalid_selector', 110 | 'name', 111 | 'result_paths', 112 | 'results_paths', 113 | 'result_pointers', 114 | 'result', 115 | 'results', 116 | 'selector', 117 | 'skip', 118 | 'tags', 119 | }; 120 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | 2 | # Contributor Covenant Code of Conduct 3 | 4 | ## Our Pledge 5 | 6 | We as members, contributors, and leaders pledge to make participation in this project and our 7 | community a harassment-free experience for everyone, regardless of age, body 8 | size, visible or invisible disability, ethnicity, sex characteristics, gender 9 | identity and expression, level of experience, education, socio-economic status, 10 | nationality, personal appearance, race, caste, color, religion, or sexual 11 | identity and orientation. 12 | 13 | We pledge to act and interact in ways that contribute to an open, welcoming, 14 | diverse, inclusive, and healthy community. 15 | 16 | ## Our Standards 17 | 18 | Examples of behavior that contributes to a positive environment for our 19 | community include: 20 | 21 | * Demonstrating empathy and kindness toward other people 22 | * Being respectful of differing opinions, viewpoints, and experiences 23 | * Giving and gracefully accepting constructive feedback 24 | * Accepting responsibility and apologizing to those affected by our mistakes, 25 | and learning from the experience 26 | * Focusing on what is best not just for us as individuals, but for the overall 27 | community 28 | 29 | Examples of unacceptable behavior include: 30 | 31 | * The use of sexualized language or imagery, and sexual attention or advances of 32 | any kind 33 | * Trolling, insulting or derogatory comments, and personal or political attacks 34 | * Public or private harassment 35 | * Publishing others' private information, such as a physical or email address, 36 | without their explicit permission 37 | * Other conduct which could reasonably be considered inappropriate in a 38 | professional setting 39 | 40 | ## Enforcement Responsibilities 41 | 42 | Community leaders are responsible for clarifying and enforcing our standards of 43 | acceptable behavior and will take appropriate and fair corrective action in 44 | response to any behavior that they deem inappropriate, threatening, offensive, 45 | or harmful. 46 | 47 | Community leaders have the right and responsibility to remove, edit, or reject 48 | comments, commits, code, wiki edits, issues, and other contributions that are 49 | not aligned to this Code of Conduct, and will communicate reasons for moderation 50 | decisions when appropriate. 51 | 52 | ## Scope 53 | 54 | This Code of Conduct applies within all community spaces, and also applies when 55 | an individual is officially representing the community in public spaces. 56 | Examples of representing our community include using an official email address, 57 | posting via an official social media account, or acting as an appointed 58 | representative at an online or offline event. 59 | 60 | ## Enforcement 61 | 62 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 63 | reported to the community leaders responsible for enforcement at 64 | [INSERT CONTACT METHOD]. 65 | All complaints will be reviewed and investigated promptly and fairly. 66 | 67 | All community leaders are obligated to respect the privacy and security of the 68 | reporter of any incident. 69 | 70 | ## Enforcement Guidelines 71 | 72 | Community leaders will follow these Community Impact Guidelines in determining 73 | the consequences for any action they deem in violation of this Code of Conduct: 74 | 75 | ### 1. Correction 76 | 77 | **Community Impact**: Use of inappropriate language or other behavior deemed 78 | unprofessional or unwelcome in the community. 79 | 80 | **Consequence**: A private, written warning from community leaders, providing 81 | clarity around the nature of the violation and an explanation of why the 82 | behavior was inappropriate. A public apology may be requested. 83 | 84 | ### 2. Warning 85 | 86 | **Community Impact**: A violation through a single incident or series of 87 | actions. 88 | 89 | **Consequence**: A warning with consequences for continued behavior. No 90 | interaction with the people involved, including unsolicited interaction with 91 | those enforcing the Code of Conduct, for a specified period of time. This 92 | includes avoiding interactions in community spaces as well as external channels 93 | like social media. Violating these terms may lead to a temporary or permanent 94 | ban. 95 | 96 | ### 3. Temporary Ban 97 | 98 | **Community Impact**: A serious violation of community standards, including 99 | sustained inappropriate behavior. 100 | 101 | **Consequence**: A temporary ban from any sort of interaction or public 102 | communication with the community for a specified period of time. No public or 103 | private interaction with the people involved, including unsolicited interaction 104 | with those enforcing the Code of Conduct, is allowed during this period. 105 | Violating these terms may lead to a permanent ban. 106 | 107 | ### 4. Permanent Ban 108 | 109 | **Community Impact**: Demonstrating a pattern of violation of community 110 | standards, including sustained inappropriate behavior, harassment of an 111 | individual, or aggression toward or disparagement of classes of individuals. 112 | 113 | **Consequence**: A permanent ban from any sort of public interaction within the 114 | community. 115 | 116 | ## Attribution 117 | 118 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 119 | version 2.1, available at 120 | [https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1]. 121 | 122 | For answers to common questions about this code of conduct, see the FAQ at 123 | [https://www.contributor-covenant.org/faq][FAQ]. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # RFC 9535 - [JSONPath]: Query Expressions for JSON in Dart 2 | [![Pub Package](https://img.shields.io/pub/v/json_path.svg)](https://pub.dev/packages/json_path) 3 | [![GitHub Issues](https://img.shields.io/github/issues/f3ath/jessie.svg)](https://github.com/f3ath/jessie/issues) 4 | [![GitHub Forks](https://img.shields.io/github/forks/f3ath/jessie.svg)](https://github.com/f3ath/jessie/network) 5 | [![GitHub Stars](https://img.shields.io/github/stars/f3ath/jessie.svg)](https://github.com/f3ath/jessie/stargazers) 6 | [![GitHub License](https://img.shields.io/badge/license-MIT-blue.svg)](https://raw.githubusercontent.com/f3ath/jessie/master/LICENSE) 7 | 8 | JSONPath defines a string syntax for selecting and extracting JSON (RFC 8259) values from within a given JSON value. 9 | 10 | This library is a Dart implementation of the RFC 9535 [JsonPath] specification. It is also expected to pass the latest version 11 | of the [Compliance Test Suite]. If you find a missing or incorrectly implemented feature, please open an issue. 12 | 13 | For installation instructions and a detailed API documentation, see the [pub.dev page](https://pub.dev/packages/json_path). 14 | 15 | ### Usage example: 16 | ```dart 17 | import 'dart:convert'; 18 | 19 | import 'package:json_path/json_path.dart'; 20 | 21 | void main() { 22 | final json = jsonDecode(''' 23 | { 24 | "store": { 25 | "book": [ 26 | { 27 | "category": "reference", 28 | "author": "Nigel Rees", 29 | "title": "Sayings of the Century", 30 | "price": 8.95 31 | }, 32 | { 33 | "category": "fiction", 34 | "author": "Evelyn Waugh", 35 | "title": "Sword of Honour", 36 | "price": 12.99 37 | }, 38 | { 39 | "category": "fiction", 40 | "author": "Herman Melville", 41 | "title": "Moby Dick", 42 | "isbn": "0-553-21311-3", 43 | "price": 8.99 44 | }, 45 | { 46 | "category": "fiction", 47 | "author": "J. R. R. Tolkien", 48 | "title": "The Lord of the Rings", 49 | "isbn": "0-395-19395-8", 50 | "price": 22.99 51 | } 52 | ], 53 | "bicycle": { 54 | "color": "red", 55 | "price": 19.95 56 | } 57 | } 58 | } 59 | '''); 60 | 61 | final prices = JsonPath(r'$..price'); 62 | 63 | print('All prices in the store:'); 64 | 65 | /// The following code will print: 66 | /// 67 | /// $['store']['book'][0]['price']: 8.95 68 | /// $['store']['book'][1]['price']: 12.99 69 | /// $['store']['book'][2]['price']: 8.99 70 | /// $['store']['book'][3]['price']: 22.99 71 | /// $['store']['bicycle']['price']: 19.95 72 | prices 73 | .read(json) 74 | .map((match) => '${match.path}:\t${match.value}') 75 | .forEach(print); 76 | } 77 | ``` 78 | 79 | 80 | ## Data manipulation 81 | Each `JsonPathMatch` produced by the `.read()` method contains the `.pointer` property which is a valid [JSON Pointer] 82 | and can be used to alter the referenced value. If you only need to manipulate JSON data, 83 | check out my [JSON Pointer implementation]. 84 | 85 | ## User-defined functions 86 | The JSONPath parser may be extended with user-defined functions. The user-defined functions 87 | take precedence over the built-in ones specified by the standard. Currently, only 88 | functions of 1 and 2 arguments are supported. 89 | 90 | To create your own function: 91 | 1. Import `package:json_path/fun_sdk.dart`. 92 | 2. Create a class implementing either `Fun1` (1 argument) or `Fun2` (2 arguments). 93 | 94 | To use it: 95 | 1. Create a new JsonPathParser with your function: `final parser = JsonPathParser(functions: [MyFunction()]);` 96 | 2. Parse the expression: `final jsonPath = parser.parse(r'$[?my_function(@)]');` 97 | 98 | For more details see the included example. 99 | 100 | This package comes with a few non-standard functions which you might find useful. 101 | - `count()` - returns the number of nodes selected by the argument 102 | - `index()` - returns the index under which the array element is referenced by the parent array 103 | - `key()` - returns the key under which the object element is referenced by the parent object 104 | - `is_array()` - returns true if the value is an array 105 | - `is_boolean()` - returns true if the value is a boolean 106 | - `is_number()` - returns true if the value is a number 107 | - `is_object()` - returns true if the value is an object 108 | - `is_string()` - returns true if the value is a string 109 | - `reverse()` - returns the reversed string 110 | - `siblings()` - returns the siblings for the nodes 111 | - `xor(, )` - returns the XOR of two booleans arguments 112 | 113 | To use them, import `package:json_path/fun_extra.dart` and pass the functions to the `JsonPath()` constructor: 114 | 115 | ```dart 116 | final jsonPath = JsonPathParser(functions: [ 117 | const Key(), 118 | const Reverse(), 119 | ]).parse(r'$[?key(@) == reverse(key(@))]'); 120 | ``` 121 | ## References 122 | - [Standard development](https://github.com/ietf-wg-jsonpath/draft-ietf-jsonpath-base) 123 | - [Feature comparison matrix](https://cburgmer.github.io/json-path-comparison/) 124 | 125 | [Compliance Test Suite]: https://github.com/jsonpath-standard/jsonpath-compliance-test-suite 126 | [JSONPath]: https://datatracker.ietf.org/doc/rfc9535/ 127 | [JSON Pointer]: https://datatracker.ietf.org/doc/html/rfc6901 128 | [JSON Pointer implementation]: https://pub.dev/packages/rfc_6901 129 | -------------------------------------------------------------------------------- /lib/src/grammar/json_path.dart: -------------------------------------------------------------------------------- 1 | import 'package:json_path/src/expression/expression.dart'; 2 | import 'package:json_path/src/expression/nodes.dart'; 3 | import 'package:json_path/src/fun/fun_call.dart'; 4 | import 'package:json_path/src/fun/fun_factory.dart'; 5 | import 'package:json_path/src/grammar/array_index.dart'; 6 | import 'package:json_path/src/grammar/array_slice.dart'; 7 | import 'package:json_path/src/grammar/child_selector.dart'; 8 | import 'package:json_path/src/grammar/comparison_expression.dart'; 9 | import 'package:json_path/src/grammar/dot_name.dart'; 10 | import 'package:json_path/src/grammar/filter_selector.dart'; 11 | import 'package:json_path/src/grammar/fun_name.dart'; 12 | import 'package:json_path/src/grammar/literal.dart'; 13 | import 'package:json_path/src/grammar/negatable.dart'; 14 | import 'package:json_path/src/grammar/parser_ext.dart'; 15 | import 'package:json_path/src/grammar/select_all_recursively.dart'; 16 | import 'package:json_path/src/grammar/sequence_selector.dart'; 17 | import 'package:json_path/src/grammar/singular_segment_sequence.dart'; 18 | import 'package:json_path/src/grammar/strings.dart'; 19 | import 'package:json_path/src/grammar/union_selector.dart'; 20 | import 'package:json_path/src/grammar/wildcard.dart'; 21 | import 'package:json_path/src/selector.dart'; 22 | import 'package:maybe_just_nothing/maybe_just_nothing.dart'; 23 | import 'package:petitparser/petitparser.dart'; 24 | 25 | class JsonPathGrammarDefinition 26 | extends GrammarDefinition> { 27 | JsonPathGrammarDefinition(this._fun); 28 | 29 | final FunFactory _fun; 30 | 31 | @override 32 | Parser> start() => _absPath().end(); 33 | 34 | Parser> _absPath() => _segmentSequence() 35 | .skip(before: char(r'$')) 36 | .map((expr) => Expression((node) => expr.call(node.root))); 37 | 38 | Parser> _segmentSequence() => 39 | _segment().star().map(sequenceSelector).map(Expression.new); 40 | 41 | Parser _segment() => [ 42 | dotName, 43 | wildcard.skip(before: char('.')), 44 | ref0(_union), 45 | ref0(_recursion), 46 | ].toChoiceParser().trim(); 47 | 48 | Parser _union() => 49 | _unionElement().toList().inBrackets().map(unionSelector); 50 | 51 | Parser _recursion() => [wildcard, _union(), memberNameShorthand] 52 | .toChoiceParser() 53 | .skip(before: string('..')) 54 | .map((value) => sequenceSelector([selectAllRecursively, value])); 55 | 56 | Parser _unionElement() => [ 57 | arraySlice, 58 | arrayIndex, 59 | wildcard, 60 | quotedString.map(childSelector), 61 | _expressionFilter(), 62 | ].toChoiceParser().trim(); 63 | 64 | Parser _expressionFilter() => 65 | _logicalExpr().skip(before: string('?').trim()).map(filterSelector); 66 | 67 | Parser> _logicalExpr() => _logicalOrSequence().map( 68 | (list) => list.reduce((a, b) => a.merge(b, (a, b) => a || b)), 69 | ); 70 | 71 | Parser>> _logicalOrSequence() => 72 | _logicalAndExpr().toList(string('||')); 73 | 74 | Parser> _logicalAndExpr() => _logicalAndSequence().map( 75 | (list) => list.reduce((a, b) => a.merge(b, (a, b) => a && b)), 76 | ); 77 | 78 | Parser>> _logicalAndSequence() => 79 | _basicExpr().toList(string('&&')); 80 | 81 | Parser> _basicExpr() => 82 | [ 83 | ref0(_parenExpr), 84 | comparisonExpression(_comparable()), 85 | _testExpr(), 86 | ].toChoiceParser( 87 | failureJoiner: (a, b) => 88 | Failure(a.buffer, a.position, 'Expression expected'), 89 | ); 90 | 91 | Parser> _parenExpr() => negatable(_logicalExpr().inParens()); 92 | 93 | Parser> _testExpr() => 94 | negatable([_existenceTest(), _logicalFunExpr()].toChoiceParser()); 95 | 96 | Parser> _existenceTest() => 97 | _filterPath().map((value) => value.map((v) => v.asLogical)); 98 | 99 | Parser> _logicalFunExpr() => _funCall(_fun.logical); 100 | 101 | Parser _funCall(T Function(FunCall) toFun) => 102 | (funName & _funArgument().toList().inParens()) 103 | .map((v) => FunCall(v[0], v[1])) 104 | .tryMap(toFun); 105 | 106 | Parser _funArgument() => [ 107 | literal, 108 | _singularFilterPath(), 109 | _filterPath(), 110 | ref0(_valueFunExpr), 111 | ref0(_logicalFunExpr), 112 | ref0(_nodesFunExpr), 113 | ref0(_logicalExpr), 114 | ].toChoiceParser().trim(); 115 | 116 | Parser> _singularFilterPath() => 117 | [ref0(_singularRelPath), ref0(_singularAbsPath)].toChoiceParser(); 118 | 119 | Parser> _valueFunExpr() => _funCall(_fun.value); 120 | 121 | Parser> _nodesFunExpr() => _funCall(_fun.nodes); 122 | 123 | Parser> _comparable() => [ 124 | literal, 125 | _singularFilterPath().map((expr) => expr.map((v) => v.asValue)), 126 | _valueFunExpr(), 127 | ].toChoiceParser(); 128 | 129 | Parser> _filterPath() => 130 | [ref0(_relPath), ref0(_absPath)].toChoiceParser(); 131 | 132 | Parser> _singularAbsPath() => 133 | singularSegmentSequence 134 | .skip(before: char(r'$'), after: _segment().not()) 135 | .map((expr) => Expression((node) => expr.call(node.root))); 136 | 137 | Parser> _relPath() => 138 | _segmentSequence().skip(before: char('@')); 139 | 140 | Parser> _singularRelPath() => 141 | singularSegmentSequence.skip(before: char('@'), after: _segment().not()); 142 | } 143 | -------------------------------------------------------------------------------- /test/cases/standard/basic.json: -------------------------------------------------------------------------------- 1 | { 2 | "tests": [ 3 | { 4 | "name": "root", 5 | "selector": "$", 6 | "document": ["first", "second"], 7 | "result": [["first", "second"]], 8 | "result_paths": ["$"], 9 | "result_pointers": [""] 10 | }, { 11 | "name": "dot field on object", 12 | "selector": "$.a", 13 | "document": {"a": "A", "b": "B"}, 14 | "result": ["A"], 15 | "result_paths": ["$['a']"], 16 | "result_pointers": ["/a"] 17 | }, { 18 | "name": "dot field on array", 19 | "selector": "$.a", 20 | "document": ["A", "B"], 21 | "result": [] 22 | }, { 23 | "name": "dot wildcard on object", 24 | "selector": "$.*", 25 | "document": {"a": "A", "b": "B"}, 26 | "result": ["A", "B"], 27 | "result_paths": ["$['a']", "$['b']"], 28 | "result_pointers": ["/a", "/b"] 29 | }, { 30 | "name": "union wildcard on array", 31 | "selector": "$[*]", 32 | "document": ["first", "second", "third", "forth", "fifth"], 33 | "result": ["first", "second", "third", "forth", "fifth"] 34 | }, { 35 | "name": "union wildcard and index on array", 36 | "selector": "$[*,1]", 37 | "document": ["first", "second", "third", "forth", "fifth"], 38 | "result": ["first", "second", "third", "forth", "fifth", "second"] 39 | }, { 40 | "name": "union wildcard on object", 41 | "selector": "$[*]", 42 | "document": {"a": "A", "b": "B"}, 43 | "result": ["A", "B"], 44 | "result_paths": ["$['a']", "$['b']"], 45 | "result_pointers": ["/a", "/b"] 46 | }, { 47 | "name": "union wildcard on object, twice", 48 | "selector": "$[*, *]", 49 | "document": {"a": "A", "b": "B"}, 50 | "result": ["A", "B", "A", "B"] 51 | }, { 52 | "name": "dot wildcard on array", 53 | "selector": "$.*", 54 | "document": ["A", "B"], 55 | "result": ["A", "B"], 56 | "result_paths": ["$[0]", "$[1]"], 57 | "result_pointers": ["/0", "/1"] 58 | }, { 59 | "name": "dot wildcard dot field", 60 | "selector": "$.*.a", 61 | "document": {"x": {"a": "Ax", "b": "Bx"}, "y": {"a": "Ay", "b": "By"}}, 62 | "result": ["Ax", "Ay"], 63 | "result_paths": ["$['x']['a']", "$['y']['a']"], 64 | "result_pointers": ["/x/a", "/y/a"] 65 | }, { 66 | "name": "union sq field on object", 67 | "selector": "$['a']", 68 | "document": {"a": "A", "b": "B"}, 69 | "result": ["A"], 70 | "result_paths": ["$['a']"], 71 | "result_pointers": ["/a"] 72 | }, { 73 | "name": "union sq field on object (2 result)", 74 | "selector": "$['a', 'c']", 75 | "document": {"a": "A", "b": "B", "c": "C"}, 76 | "result": ["A", "C"], 77 | "result_paths": ["$['a']", "$['c']"], 78 | "result_pointers": ["/a", "/c"] 79 | }, { 80 | "name": "union sq field on object (numeric key)", 81 | "selector": "$['1']", 82 | "document": {"0": "A", "1": "B"}, 83 | "result": ["B"], 84 | "result_paths": ["$['1']"], 85 | "result_pointers": ["/1"] 86 | }, { 87 | "name": "union sq field on array", 88 | "selector": "$['a']", 89 | "document": ["A", "B"], 90 | "result": [] 91 | }, { 92 | "name": "union dq field on object", 93 | "selector": "$[\"a\"]", 94 | "document": {"a": "A", "b": "B"}, 95 | "result": ["A"], 96 | "result_paths": ["$['a']"], 97 | "result_pointers": ["/a"] 98 | }, { 99 | "name": "union dq field on object (2 result)", 100 | "selector": "$[\"a\", \"c\"]", 101 | "document": {"a": "A", "b": "B", "c": "C"}, 102 | "result": ["A", "C"], 103 | "result_paths": ["$['a']", "$['c']"], 104 | "result_pointers": ["/a", "/c"] 105 | }, { 106 | "name": "union dq field on object (numeric key)", 107 | "selector": "$[\"1\"]", 108 | "document": {"0": "A", "1": "B"}, 109 | "result": ["B"], 110 | "result_paths": ["$['1']"], 111 | "result_pointers": ["/1"] 112 | }, { 113 | "name": "union dq field on array", 114 | "selector": "$[\"a\"]", 115 | "document": ["A", "B"], 116 | "result": [] 117 | }, { 118 | "name": "union index on array", 119 | "selector": "$[1]", 120 | "document": ["A", "B"], 121 | "result": ["B"], 122 | "result_paths": ["$[1]"], 123 | "result_pointers": ["/1"] 124 | }, { 125 | "name": "union index on object", 126 | "selector": "$[1]", 127 | "document": {"a": "A", "b": "B"}, 128 | "result": [] 129 | }, { 130 | "name": "union index and field on object", 131 | "selector": "$[\"0\", 1, 'a']", 132 | "document": {"a": "A", "b": "B", "0": "Zero", "1": "One"}, 133 | "result": ["Zero", "A"], 134 | "result_paths": ["$['0']", "$['a']"], 135 | "result_pointers": ["/0", "/a"] 136 | }, { 137 | "name": "union index and field on array", 138 | "selector": "$[1, 'a', '1']", 139 | "document": ["A", "B"], 140 | "result": ["B"], 141 | "result_paths": ["$[1]"], 142 | "result_pointers": ["/1"] 143 | }, { 144 | "name": "union array slice on array [15:-7:-3]", 145 | "selector": "$[15:-7:-3]", 146 | "document": [0, 1, 2, 3, 4, 5, 6, 7, 8, 9], 147 | "result": [9, 6], 148 | "result_paths": ["$[9]", "$[6]"], 149 | "result_pointers": ["/9", "/6"] 150 | }, { 151 | "name": "union array slice on array [:]", 152 | "selector": "$[:]", 153 | "document": [0, 1, 2], 154 | "result": [0, 1, 2], 155 | "result_paths": ["$[0]", "$[1]", "$[2]"], 156 | "result_pointers": ["/0", "/1", "/2"] 157 | }, { 158 | "name": "test [:]", 159 | "selector": "$.Table1[:]", 160 | "document": {"Table1": [0, 1, 2]}, 161 | "result": [0, 1, 2] 162 | }, { 163 | "name": "union array slice on array [::]", 164 | "selector": "$[:10000000000:]", 165 | "document": [0, 1, 2], 166 | "result": [0, 1, 2], 167 | "result_paths": ["$[0]", "$[1]", "$[2]"], 168 | "result_pointers": ["/0", "/1", "/2"] 169 | }, { 170 | "name": "recursion wildcard", 171 | "selector": "$..*", 172 | "document": {"a": {"foo": "bar"}, "b": [42]}, 173 | "result": [{"foo": "bar"}, [42], "bar", 42], 174 | "result_paths": ["$['a']", "$['b']", "$['a']['foo']", "$['b'][0]"], 175 | "result_pointers": ["/a", "/b", "/a/foo", "/b/0"] 176 | }, { 177 | "name": "recursion union", 178 | "selector": "$..[0]", 179 | "document": {"a": {"foo": "bar"}, "b": [42]}, 180 | "result": [42], 181 | "result_paths": ["$['b'][0]"], 182 | "result_pointers": ["/b/0"] 183 | } 184 | ] 185 | } -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 5 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 6 | 7 | ## [0.9.0] - 2025-12-10 8 | ### Changed 9 | - Minor improvements in typing 10 | - Bumped the min SDK version to 3.10 11 | - Bumped maybe\_just\_nothing to 0.6 12 | 13 | ## [0.8.0] - 2025-07-06 14 | ### Changed 15 | - Bumped petitparser to 7.0 16 | - Bumped SDK to ^3.8 17 | 18 | ## [0.7.6] - 2025-05-26 19 | ### Changed 20 | - Bumped dependencies and CTS 21 | 22 | ## [0.7.5] - 2025-01-28 23 | ### Changed 24 | - Minor performance improvements 25 | - CTS update 26 | 27 | ### Fixed 28 | - Slash escaped incorrectly in normalized paths ([issue](https://github.com/f3ath/jessie/issues/122)) 29 | 30 | ## [0.7.4] - 2024-08-03 31 | ### Changed 32 | - CTS updated 33 | 34 | ### Fixed 35 | - Uppercase "E" not acceptable in floating point numbers 36 | - A digit before the decimal point not enforced for floating point numbers 37 | - Integer literal bounds not enforced 38 | 39 | ## [0.7.3] - 2024-08-01 40 | ### Changed 41 | - Updated CTS 42 | 43 | ### Fixed 44 | - Invalid escape sequences were allowed in child selectors. See https://github.com/jsonpath-standard/jsonpath-compliance-test-suite/pull/87 45 | 46 | ## [0.7.2] - 2024-05-30 47 | ### Added 48 | - New functions: `key()` and `index()` 49 | 50 | ## [0.7.1] - 2024-03-02 51 | ### Changed 52 | - Bumped the CTS to the latest 53 | 54 | ## [0.7.0] - 2023-12-29 55 | ### Changed 56 | - Renamed `Nodes` to `NodeList` 57 | - Bumped the CTS to the latest version 58 | 59 | ## [0.6.6] - 2023-09-23 60 | ### Fixed 61 | - Logical expressions should be allowed in function arguments 62 | 63 | ## [0.6.5] - 2023-09-11 64 | ### Fixed 65 | - Certain numbers were not parsed correctly 66 | 67 | ## [0.6.4] - 2023-08-26 68 | ### Changed 69 | - Bump dependencies versions 70 | 71 | ## [0.6.3] - 2023-08-26 72 | ### Fixed 73 | - Allow whitespaces after `?` in expressions 74 | 75 | ## [0.6.2] - 2023-07-21 76 | ### Fixed 77 | - Disallow comparison of non-singular queries 78 | 79 | ## [0.6.1] - 2023-07-17 80 | ### Fixed 81 | - Allow whitespace in between selector segments 82 | 83 | ## [0.6.0] - 2023-05-27 84 | ### Changed 85 | - Bump SDK version to 3.0.0 86 | - Disallow whitespace between the function name and the parentheses 87 | - Disallow whitespace between the expression and the brackets 88 | - `search()` and `match()` now strictly follow the I-Regexp convention. Expressions not conforming to I-Regexp will yield `false` regardless of the value 89 | 90 | ## [0.5.3] - 2023-04-29 \[YANKED\] 91 | ### Changed 92 | - `search()` and `match()` now strictly follow the I-Regexp convention. Expressions not conforming to I-Regexp will yield `false` regardless of the value 93 | 94 | ## [0.5.2] - 2023-04-05 95 | ### Added 96 | - Improved IRegex support 97 | 98 | ## [0.5.1] - 2023-03-28 99 | ### Added 100 | - Better support for Normalized Paths 101 | 102 | ## [0.5.0] - 2023-03-23 103 | ### Added 104 | - Full support of some built-in functions: `length()`, `size()`, `search()`, `match()`, `value()`. 105 | - Basic support of custom user-defined functions. 106 | 107 | ### Changed 108 | - BC-BREAKING! The package is now following the [IETF JSON Path spec](https://www.ietf.org/archive/id/draft-ietf-jsonpath-base-10.html) 109 | which means a lot of internal and BC-breaking changes. Please refer to the tests and examples. 110 | 111 | ### Removed 112 | - BC-BREAKING! Support of the callback filters has been dropped. Use custom functions instead. 113 | 114 | ## [0.4.4] - 2023-03-18 115 | ### Fixed 116 | - Reverted changes from 0.4.3 as they caused dependency issues. 117 | 118 | ## [0.4.3] - 2023-03-18 119 | ### Fixed 120 | - Deprecation warnings from petitparser. 121 | 122 | ## [0.4.2] - 2022-08-03 123 | ### Added 124 | - Expressions enhancements: float literals, negation, parenthesis. 125 | 126 | ## [0.4.1] - 2022-06-14 127 | ### Added 128 | - Lower case hex support 129 | 130 | ### Changed 131 | - Updated CTS to latest 132 | 133 | ## [0.4.0] - 2022-03-21 134 | ### Changed 135 | - Dart 2.16 136 | - Dependency bump: petitparser 5.0.0 137 | 138 | ## [0.3.1] - 2021-12-18 139 | ### Added 140 | - Filtering expressions 141 | 142 | ### Changed 143 | - Require dart 2.15 144 | 145 | ## [0.3.0] - 2021-02-18 146 | ### Added 147 | - `JsonPathMatch.context` contains the matching context. It is intended to be used in named filters. 148 | - `JsonPathMatch.parent` contains the parent match. 149 | - `JsonPathMatch.pointer` contains the RFC 6901 JSON Pointer to the match. 150 | - Very basic support for evaluated expressions 151 | 152 | ### Changed 153 | - Named filters argument renamed from `filter` to `filters` 154 | - Named filters can now be passed to the `read()` method. 155 | - Named filters callback now accepts the entire `JsonPathMatch` object, not just the value. 156 | 157 | ### Removed 158 | - The `set()` method. Use the `pointer` property instead. 159 | 160 | ## [0.2.0] - 2020-09-07 161 | ### Added 162 | - Ability to create arrays and set adjacent indices 163 | 164 | ### Changed 165 | - List union sorts the keys 166 | 167 | ### Fixed 168 | - Improved union parsing stability 169 | 170 | ## [0.1.2] - 2020-09-06 171 | ### Changed 172 | - When JsonPath.set() is called on a path with non-existing property, the property will be created. 173 | Previously, no modification would be made and no errors/exceptions thrown. 174 | - When JsonPath.set() is called on a path with non-existing index, a `RangeError` will be thrown. 175 | Previously, no modification would be made and no errors/exceptions thrown. 176 | 177 | ## [0.1.1] - 2020-09-05 178 | ### Fixed 179 | - Fixed example code in the readme 180 | 181 | ## [0.1.0] - 2020-09-05 182 | ### Added 183 | - JsonPath.set() method to alter the JSON object in a non-destructive way 184 | 185 | ### Changed 186 | - **BREAKING!** `Result` renamed to `JsonPathMatch` 187 | - **BREAKING!** `JsonPath.filter()` renamed to `read()` 188 | 189 | ## [0.0.2] - 2020-09-01 190 | ### Fixed 191 | - Last element of array would not get selected (regression #1) 192 | 193 | ## [0.0.1] - 2020-08-03 194 | ### Added 195 | - Filters 196 | 197 | ## [0.0.0+dev.7] - 2020-08-02 198 | ### Changed 199 | - Tokenized and AST refactoring 200 | 201 | ## [0.0.0+dev.6] - 2020-08-01 202 | ### Added 203 | - Unions 204 | 205 | ## [0.0.0+dev.5] - 2020-07-31 206 | ### Added 207 | - Slice expression 208 | 209 | ## [0.0.0+dev.4] - 2020-07-29 210 | ### Added 211 | - Bracket field notation support 212 | 213 | ## [0.0.0+dev.3] - 2020-07-28 214 | ### Added 215 | - Partial implementation of bracket field notation 216 | 217 | ## [0.0.0+dev.2] - 2020-07-28 218 | ### Added 219 | - Recursive selector 220 | - Wildcard selector 221 | 222 | ## [0.0.0+dev.1] - 2020-07-27 223 | ### Added 224 | - Tokenizer and AST 225 | - All-in-array selector 226 | 227 | ## 0.0.0+dev.0 - 2020-07-24 228 | ### Added 229 | - Basic design draft 230 | 231 | [0.9.0]: https://github.com/f3ath/jessie/compare/0.8.0...0.9.0 232 | [0.8.0]: https://github.com/f3ath/jessie/compare/0.7.6...0.8.0 233 | [0.7.6]: https://github.com/f3ath/jessie/compare/0.7.5...0.7.6 234 | [0.7.5]: https://github.com/f3ath/jessie/compare/0.7.4...0.7.5 235 | [0.7.4]: https://github.com/f3ath/jessie/compare/0.7.3...0.7.4 236 | [0.7.3]: https://github.com/f3ath/jessie/compare/0.7.2...0.7.3 237 | [0.7.2]: https://github.com/f3ath/jessie/compare/0.7.1...0.7.2 238 | [0.7.1]: https://github.com/f3ath/jessie/compare/0.7.0...0.7.1 239 | [0.7.0]: https://github.com/f3ath/jessie/compare/0.6.6...0.7.0 240 | [0.6.6]: https://github.com/f3ath/jessie/compare/0.6.5...0.6.6 241 | [0.6.5]: https://github.com/f3ath/jessie/compare/0.6.4...0.6.5 242 | [0.6.4]: https://github.com/f3ath/jessie/compare/0.6.3...0.6.4 243 | [0.6.3]: https://github.com/f3ath/jessie/compare/0.6.2...0.6.3 244 | [0.6.2]: https://github.com/f3ath/jessie/compare/0.6.1...0.6.2 245 | [0.6.1]: https://github.com/f3ath/jessie/compare/0.6.0...0.6.1 246 | [0.6.0]: https://github.com/f3ath/jessie/compare/0.5.3...0.6.0 247 | [0.5.3]: https://github.com/f3ath/jessie/compare/0.5.2...0.5.3 248 | [0.5.2]: https://github.com/f3ath/jessie/compare/0.5.1...0.5.2 249 | [0.5.1]: https://github.com/f3ath/jessie/compare/0.5.0...0.5.1 250 | [0.5.0]: https://github.com/f3ath/jessie/compare/0.4.4...0.5.0 251 | [0.4.4]: https://github.com/f3ath/jessie/compare/0.4.3...0.4.4 252 | [0.4.3]: https://github.com/f3ath/jessie/compare/0.4.2...0.4.3 253 | [0.4.2]: https://github.com/f3ath/jessie/compare/0.4.1...0.4.2 254 | [0.4.1]: https://github.com/f3ath/jessie/compare/0.4.0...0.4.1 255 | [0.4.0]: https://github.com/f3ath/jessie/compare/0.3.1...0.4.0 256 | [0.3.1]: https://github.com/f3ath/jessie/compare/0.3.0...0.3.1 257 | [0.3.0]: https://github.com/f3ath/jessie/compare/0.2.0...0.3.0 258 | [0.2.0]: https://github.com/f3ath/jessie/compare/0.1.2...0.2.0 259 | [0.1.2]: https://github.com/f3ath/jessie/compare/0.1.1...0.1.2 260 | [0.1.1]: https://github.com/f3ath/jessie/compare/0.1.0...0.1.1 261 | [0.1.0]: https://github.com/f3ath/jessie/compare/0.0.2...0.1.0 262 | [0.0.2]: https://github.com/f3ath/jessie/compare/0.0.1...0.0.2 263 | [0.0.1]: https://github.com/f3ath/jessie/compare/0.0.0+dev.7...0.0.1 264 | [0.0.0+dev.7]: https://github.com/f3ath/jessie/compare/0.0.0+dev.6...0.0.0+dev.7 265 | [0.0.0+dev.6]: https://github.com/f3ath/jessie/compare/0.0.0+dev.5...0.0.0+dev.6 266 | [0.0.0+dev.5]: https://github.com/f3ath/jessie/compare/0.0.0+dev.4...0.0.0+dev.5 267 | [0.0.0+dev.4]: https://github.com/f3ath/jessie/compare/0.0.0+dev.3...0.0.0+dev.4 268 | [0.0.0+dev.3]: https://github.com/f3ath/jessie/compare/0.0.0+dev.2...0.0.0+dev.3 269 | [0.0.0+dev.2]: https://github.com/f3ath/jessie/compare/0.0.0+dev.1...0.0.0+dev.2 270 | [0.0.0+dev.1]: https://github.com/f3ath/jessie/compare/0.0.0+dev.0...0.0.0+dev.1 271 | -------------------------------------------------------------------------------- /test/cases/standard/expressions_equality.json: -------------------------------------------------------------------------------- 1 | { 2 | "tests": [ 3 | { 4 | "name": "tautology means true", 5 | "selector": "$[?(1==1)]", 6 | "document": [42], 7 | "result": [42], 8 | "result_paths": ["$[0]"], 9 | "result_pointers": ["/0"] 10 | }, { 11 | "name": "object itself", 12 | "selector": "$[?(@ == 1)]", 13 | "document": [0, 1, 2, "1", "a", "0", ""], 14 | "result": [1] 15 | }, { 16 | "name": "array index", 17 | "selector": "$[?(@[1] == 'b')]", 18 | "document": [["a", "b"], ["x", "y"]], 19 | "result": [["a", "b"]], 20 | "result_paths": ["$[0]"], 21 | "result_pointers": ["/0"] 22 | }, { 23 | "name": "array index with odd spacing", 24 | "selector": "$[?(@ [1] =='b')]", 25 | "document": [["a", "b"], ["x", "y"]], 26 | "result": [["a", "b"]], 27 | "result_paths": ["$[0]"], 28 | "result_pointers": ["/0"] 29 | }, { 30 | "name": "object child, single quote", 31 | "selector": "$[?(@['key'] == 'b')]", 32 | "document": [{"key": "a"}, {"key": "b"}, {}], 33 | "result": [{"key": "b"}], 34 | "result_paths": ["$[1]"], 35 | "result_pointers": ["/1"] 36 | }, { 37 | "name": "object child, single quote, odd spacing", 38 | "selector": "$[?(@ ['key']=='b')]", 39 | "document": [{"key": "a"}, {"key": "b"}, {}], 40 | "result": [{"key": "b"}], 41 | "result_paths": ["$[1]"], 42 | "result_pointers": ["/1"] 43 | }, { 44 | "name": "object child, double quote", 45 | "selector": "$[?(@[\"key\"] == 'b')]", 46 | "document": [{"key": "a"}, {"key": "b"}, {}], 47 | "result": [{"key": "b"}], 48 | "result_paths": ["$[1]"], 49 | "result_pointers": ["/1"] 50 | }, { 51 | "name": "object child, dot-notation", 52 | "selector": "$[?(@.key=='b')]", 53 | "document": [{"key": "a"}, {"key": "b"}, {}], 54 | "result": [{"key": "b"}], 55 | "result_paths": ["$[1]"], 56 | "result_pointers": ["/1"] 57 | }, { 58 | "name": "object child, dot-notation, depth 2", 59 | "selector": "$[?(@.foo.bar=='b')]", 60 | "document": [{"foo": {"bar": "a"}}, {"foo": {"bar": "b"}}, {"foo": "b"} ,{}], 61 | "result": [{"foo": {"bar": "b"}}], 62 | "result_paths": ["$[1]"], 63 | "result_pointers": ["/1"] 64 | }, { 65 | "name": "object child, dot-notation, depth 2, odd spacing", 66 | "selector": "$[?(@ \t .foo\n .bar== 'b')]", 67 | "document": [{"foo": {"bar": "a"}}, {"foo": {"bar": "b"}}, {"foo": "b"} ,{}], 68 | "result": [{"foo": {"bar": "b"}}], 69 | "result_paths": ["$[1]"], 70 | "result_pointers": ["/1"] 71 | }, { 72 | "name": "object child, dot-notation, int value", 73 | "selector": "$[?(@.id==42)].name", 74 | "document": [{"id": 42, "name": "forty-two"}, {"id": 1, "name": "one"}], 75 | "result": ["forty-two"], 76 | "result_paths": ["$[0]['name']"], 77 | "result_pointers": ["/0/name"] 78 | }, { 79 | "name": "object child, combined", 80 | "selector": "$[?(@.foo['bar'] == 'b')]", 81 | "document": [{"foo": {"bar": "a"}}, {"foo": {"bar": "b"}}, {"foo": {"moo": 42}}, {}], 82 | "result": [{"foo": {"bar": "b"}}], 83 | "result_paths": ["$[1]"], 84 | "result_pointers": ["/1"] 85 | }, { 86 | "name": "equal props", 87 | "selector": "$[?(@.foo == @.bar)]", 88 | "document": [{"foo": 1, "bar": 2}, {"foo": 42, "bar": 42}, {"foo": 1, "bro": 1}, {}], 89 | "result": [{"foo": 42, "bar": 42}, {}], 90 | "result_paths": ["$[1]", "$[3]"], 91 | "result_pointers": ["/1", "/3"] 92 | }, { 93 | "selector": "$[?(1!=1)]", 94 | "document": [42], 95 | "result": [] 96 | }, { 97 | "selector": "$[?(@ != 1)]", 98 | "document": [0, 1, 2, "1", "a", "0", ""], 99 | "result": [0, 2, "1", "a", "0", ""] 100 | }, { 101 | "selector": "$[?(@ == -1)]", 102 | "document": [0, -1, 2, "1", "a", "0", ""], 103 | "result": [-1] 104 | }, { 105 | "selector": "$[?(@ == 0)]", 106 | "document": [0, -1, 2, "1", "a", "0", ""], 107 | "result": [0] 108 | }, { 109 | "selector": "$[?@.a == @.b]", 110 | "document": [ 111 | {"a": [], "b": []}, 112 | {"a": []}, 113 | {"a": [1], "b": []}, 114 | {"a": [null], "b": []}, 115 | {"a": [null]}, 116 | {"a": [null], "b": [null]} 117 | ], 118 | "result": [{"a": [], "b": []}, {"a": [null], "b": [null]}] 119 | }, { 120 | "selector": "$[?@.a == @.b]", 121 | "document": [ 122 | {"a": {}, "b": {}}, 123 | {"a": {}}, 124 | {"a": {"x": 1}, "b": {}}, 125 | {"a": {"": null}, "b": {}}, 126 | {"a": {"": null}}, 127 | {"a": {"": null}, "b": {"": null}} 128 | ], 129 | "result": [{"a": {}, "b": {}}, {"a": {"": null}, "b": {"": null}}] 130 | }, { 131 | "selector": "$[?(@ != '1')]", 132 | "document": [0, 1, 2, "1", "a", "0", ""], 133 | "result": [0, 1, 2, "a", "0", ""] 134 | }, { 135 | "selector": "$[?(@.foo != @.bar)]", 136 | "document": [{"foo": 1, "bar": 2}, {"foo": 42, "bar": 42}, {"foo": 1, "bro": 1}, {}], 137 | "result": [{"foo": 1, "bar": 2}, {"foo": 1, "bro": 1}] 138 | }, { 139 | "name": "relative non-singular query, index, equal", 140 | "selector": "$[?(@[0, 0]==42)]", 141 | "invalid_selector": true 142 | }, { 143 | "name": "relative non-singular query, index, not equal", 144 | "selector": "$[?(@[0, 0]!=42)]", 145 | "invalid_selector": true 146 | }, { 147 | "name": "relative non-singular query, index, less-or-equal", 148 | "selector": "$[?(@[0, 0]<=42)]", 149 | "invalid_selector": true 150 | }, { 151 | "name": "relative non-singular query, name, equal", 152 | "selector": "$[?(@['a', 'a']==42)]", 153 | "invalid_selector": true 154 | }, { 155 | "name": "relative non-singular query, name, not equal", 156 | "selector": "$[?(@['a', 'a']!=42)]", 157 | "invalid_selector": true 158 | }, { 159 | "name": "relative non-singular query, name, less-or-equal", 160 | "selector": "$[?(@['a', 'a']<=42)]", 161 | "invalid_selector": true 162 | }, { 163 | "name": "relative non-singular query, combined, equal", 164 | "selector": "$[?(@[0, '0']==42)]", 165 | "invalid_selector": true 166 | }, { 167 | "name": "relative non-singular query, combined, not equal", 168 | "selector": "$[?(@[0, '0']!=42)]", 169 | "invalid_selector": true 170 | }, { 171 | "name": "relative non-singular query, combined, less-or-equal", 172 | "selector": "$[?(@[0, '0']<=42)]", 173 | "invalid_selector": true 174 | }, { 175 | "name": "relative non-singular query, wildcard, equal", 176 | "selector": "$[?(@.*==42)]", 177 | "invalid_selector": true 178 | }, { 179 | "name": "relative non-singular query, wildcard, not equal", 180 | "selector": "$[?(@.*!=42)]", 181 | "invalid_selector": true 182 | }, { 183 | "name": "relative non-singular query, wildcard, less-or-equal", 184 | "selector": "$[?(@.*<=42)]", 185 | "invalid_selector": true 186 | }, { 187 | "name": "relative non-singular query, slice, equal", 188 | "selector": "$[?(@[0:0]==42)]", 189 | "invalid_selector": true 190 | }, { 191 | "name": "relative non-singular query, slice, not equal", 192 | "selector": "$[?(@[0:0]!=42)]", 193 | "invalid_selector": true 194 | }, { 195 | "name": "relative non-singular query, slice, less-or-equal", 196 | "selector": "$[?(@[0:0]<=42)]", 197 | "invalid_selector": true 198 | }, { 199 | "name": "absolute non-singular query, index, equal", 200 | "selector": "$[?($[0, 0]==42)]", 201 | "invalid_selector": true 202 | }, { 203 | "name": "absolute non-singular query, index, not equal", 204 | "selector": "$[?($[0, 0]!=42)]", 205 | "invalid_selector": true 206 | }, { 207 | "name": "absolute non-singular query, index, less-or-equal", 208 | "selector": "$[?($[0, 0]<=42)]", 209 | "invalid_selector": true 210 | }, { 211 | "name": "absolute non-singular query, name, equal", 212 | "selector": "$[?($['a', 'a']==42)]", 213 | "invalid_selector": true 214 | }, { 215 | "name": "absolute non-singular query, name, not equal", 216 | "selector": "$[?($['a', 'a']!=42)]", 217 | "invalid_selector": true 218 | }, { 219 | "name": "absolute non-singular query, name, less-or-equal", 220 | "selector": "$[?($['a', 'a']<=42)]", 221 | "invalid_selector": true 222 | }, { 223 | "name": "absolute non-singular query, combined, equal", 224 | "selector": "$[?($[0, '0']==42)]", 225 | "invalid_selector": true 226 | }, { 227 | "name": "absolute non-singular query, combined, not equal", 228 | "selector": "$[?($[0, '0']!=42)]", 229 | "invalid_selector": true 230 | }, { 231 | "name": "absolute non-singular query, combined, less-or-equal", 232 | "selector": "$[?($[0, '0']<=42)]", 233 | "invalid_selector": true 234 | }, { 235 | "name": "absolute non-singular query, wildcard, equal", 236 | "selector": "$[?($.*==42)]", 237 | "invalid_selector": true 238 | }, { 239 | "name": "absolute non-singular query, wildcard, not equal", 240 | "selector": "$[?($.*!=42)]", 241 | "invalid_selector": true 242 | }, { 243 | "name": "absolute non-singular query, wildcard, less-or-equal", 244 | "selector": "$[?($.*<=42)]", 245 | "invalid_selector": true 246 | }, { 247 | "name": "absolute non-singular query, slice, equal", 248 | "selector": "$[?($[0:0]==42)]", 249 | "invalid_selector": true 250 | }, { 251 | "name": "absolute non-singular query, slice, not equal", 252 | "selector": "$[?($[0:0]!=42)]", 253 | "invalid_selector": true 254 | }, { 255 | "name": "absolute non-singular query, slice, less-or-equal", 256 | "selector": "$[?($[0:0]<=42)]", 257 | "invalid_selector": true 258 | } 259 | ] 260 | } --------------------------------------------------------------------------------