From 79970d0cec0bbc78b8e577eb5efc7fb5d15a8140 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?The=20=D0=9A=D0=BE=D0=BD=D1=8C?= Date: Fri, 8 Jan 2021 00:09:03 -0800 Subject: [PATCH] Migrate to petitparser (#18) --- .github/workflows/dart.yml | 5 +- .gitmodules | 3 + CHANGELOG.md | 12 +- LICENSE | 2 +- README.md | 56 +---- cts | 1 + example/main.dart | 25 -- lib/json_path.dart | 4 +- lib/json_pointer.dart | 18 ++ lib/src/any_match.dart | 30 +++ lib/src/ast/ast.dart | 25 -- lib/src/ast/node.dart | 19 -- lib/src/ast/tokenize.dart | 17 -- lib/src/build_parser.dart | 7 + lib/src/filter_not_found.dart | 8 + lib/src/grammar.dart | 117 +++++++++ lib/src/json_path.dart | 42 ++-- lib/src/json_path_match.dart | 24 +- lib/src/match_factory.dart | 92 +++++++ lib/src/matching_context.dart | 11 + lib/src/parsing_state.dart | 123 ---------- lib/src/predicate.dart | 1 - lib/src/quote.dart | 27 +- lib/src/selector/array_index.dart | 19 ++ lib/src/selector/array_slice.dart | 57 +++++ lib/src/selector/field.dart | 16 ++ lib/src/selector/filter.dart | 24 -- lib/src/selector/joint.dart | 39 --- lib/src/selector/list_union.dart | 43 ---- lib/src/selector/list_wildcard.dart | 24 -- lib/src/selector/named_filter.dart | 18 ++ lib/src/selector/object_union.dart | 33 --- lib/src/selector/object_wildcard.dart | 30 --- lib/src/selector/recursion.dart | 17 ++ lib/src/selector/recursive.dart | 48 ---- lib/src/selector/root.dart | 17 -- lib/src/selector/selector.dart | 14 +- lib/src/selector/selector_mixin.dart | 8 - lib/src/selector/sequence.dart | 18 ++ lib/src/selector/slice.dart | 63 ----- lib/src/selector/union.dart | 12 + lib/src/selector/wildcard.dart | 17 ++ pubspec.yaml | 7 +- test/cases/basic.json | 172 +++++++++++++ test/cases/unicode.json | 10 + test/cases_test.dart | 65 +++++ test/cts_test.dart | 31 +++ test/filter_test.dart | 339 -------------------------- test/filters_test.dart | 51 ++++ test/parser_test.dart | 71 ++++++ test/parsing_test.dart | 9 - test/set_test.dart | 153 ------------ 52 files changed, 940 insertions(+), 1154 deletions(-) create mode 100644 .gitmodules create mode 160000 cts create mode 100644 lib/json_pointer.dart create mode 100644 lib/src/any_match.dart delete mode 100644 lib/src/ast/ast.dart delete mode 100644 lib/src/ast/node.dart delete mode 100644 lib/src/ast/tokenize.dart create mode 100644 lib/src/build_parser.dart create mode 100644 lib/src/filter_not_found.dart create mode 100644 lib/src/grammar.dart create mode 100644 lib/src/match_factory.dart create mode 100644 lib/src/matching_context.dart delete mode 100644 lib/src/parsing_state.dart delete mode 100644 lib/src/predicate.dart create mode 100644 lib/src/selector/array_index.dart create mode 100644 lib/src/selector/array_slice.dart create mode 100644 lib/src/selector/field.dart delete mode 100644 lib/src/selector/filter.dart delete mode 100644 lib/src/selector/joint.dart delete mode 100644 lib/src/selector/list_union.dart delete mode 100644 lib/src/selector/list_wildcard.dart create mode 100644 lib/src/selector/named_filter.dart delete mode 100644 lib/src/selector/object_union.dart delete mode 100644 lib/src/selector/object_wildcard.dart create mode 100644 lib/src/selector/recursion.dart delete mode 100644 lib/src/selector/recursive.dart delete mode 100644 lib/src/selector/root.dart delete mode 100644 lib/src/selector/selector_mixin.dart create mode 100644 lib/src/selector/sequence.dart delete mode 100644 lib/src/selector/slice.dart create mode 100644 lib/src/selector/union.dart create mode 100644 lib/src/selector/wildcard.dart create mode 100644 test/cases/basic.json create mode 100644 test/cases/unicode.json create mode 100644 test/cases_test.dart create mode 100644 test/cts_test.dart delete mode 100644 test/filter_test.dart create mode 100644 test/filters_test.dart create mode 100644 test/parser_test.dart delete mode 100644 test/parsing_test.dart delete mode 100644 test/set_test.dart diff --git a/.github/workflows/dart.yml b/.github/workflows/dart.yml index 65727ed..ce74b02 100644 --- a/.github/workflows/dart.yml +++ b/.github/workflows/dart.yml @@ -4,7 +4,6 @@ on: push: branches: [ master ] pull_request: - branches: [ master ] jobs: build: @@ -16,6 +15,10 @@ jobs: steps: - uses: actions/checkout@v2 + - name: Update submodules + run: git submodule update --init --recursive + - name: Print Dart version + run: dart --version - name: Install dependencies run: dart pub get - name: Format diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..11520cd --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "cts"] + path = cts + url = https://github.com/jsonpath-standard/jsonpath-compliance-test-suite.git diff --git a/CHANGELOG.md b/CHANGELOG.md index 80c2221..7260a42 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,16 @@ ## [Unreleased] +### Added +- `JsonPathMatch.context` contains the matching context. It is intended to be used in named filters. +- `JsonPathMatch.parent` contains the parent match. +- `JsonPathMatch.pointer` contains the RFC 6901 JSON Pointer to the match. + ### Changed -- Prepared for null safety +- Named filters argument renamed from `filter` to `filters` +- Named filters can now be passed to the `read()` method. +- Named filters callback now accepts the entire `JsonPathMatch` object, not just the value. + +### Removed +- The `set()` method. The intention is to allow modifications via JSON Pointer in the future. ## [0.2.0] - 2020-09-07 ### Added diff --git a/LICENSE b/LICENSE index 1d70fbf..18474c4 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2020 The Конь +Copyright (c) 2020-2021 Alexey Karapetov Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index da602d0..d5c7569 100644 --- a/README.md +++ b/README.md @@ -58,66 +58,20 @@ void main() { /// $['store']['bicycle']['price']: 19.95 prices .read(json) - .map((result) => '${result.path}:\t${result.value}') + .map((match) => '${match.path}:\t${match.value}') .forEach(print); - - print('\n'); - - final bikeColor = JsonPath(r'$.store.bicycle.color'); - - print('A copy of the store with repainted bike:'); - print(bikeColor.set(json, 'blue')); - - print('\n'); - - print('Note, that the bike in the original store is still red:'); - bikeColor - .read(json) - .map((result) => '${result.path}:\t${result.value}') - .forEach(print); - - print('\n'); - - print('It is also possible to modify json in place:'); - final someBooks = JsonPath(r'$.store.book[::2]'); - someBooks.read(json).forEach((match) { - result.value['title'] = 'Banana'; - }); - print(json); } - ``` ## Features and limitations Generally, this library tries to mimic the [reference implementations], except for the filtering. -Evaluated expressions are not supported, use named filters instead (see below). -### Fields and indices -Both dot-notation (`$.store.book[0].title`) and bracket-notation (`$['store']['book'][2]['title']`) are supported. - -- The dot-notation only recognizes alphanumeric fields starting with a letter. Use bracket-notation for general cases. -- The bracket-notation supports only single quotes. - -### Wildcards -Wildcards (`*`) can be used for objects (`$.store.*`) and arrays (`$.store.book[*]`); - -### Recursion -Use `..` to iterate all elements recursively. E.g. `$.store..price` matches all prices in the store. - -### Array slice -Use `[start:end:step]` to filter arrays. Any index can be omitted E.g. `$.store.book[3::2]` selects all even books -starting from the 4th. Negative `start` and `end` are also supported. - -### Unions -Array (`book[0,1]`) and object (`book[author,title,price]`) unions are supported. Array unions are always sorted. -Object unions support the bracket-notation. - -### Filtering -Due to the nature of Dart language, filtering expressions like `$..book[?(@.price<10)]` are NOT supported. +Evaluated filtering expressions like `$..book[?(@.price<10)]` are NOT yet supported. Instead, use the callback-kind of filters. ```dart /// Select all elements with price under 20 -JsonPath(r'$.store..[?discounted]', filter: { - 'discounted': (e) => e is Map && e['price'] is num && e['price'] < 20 +final path = JsonPath(r'$.store..[?discounted]', filters: { +'discounted': (match) => + match.value is Map && match.value['price'] is num && match.value['price'] < 20 }); ``` diff --git a/cts b/cts new file mode 160000 index 0000000..ab56e44 --- /dev/null +++ b/cts @@ -0,0 +1 @@ +Subproject commit ab56e44bf7ea9997f2f3a10bbc60ee317c151a61 diff --git a/example/main.dart b/example/main.dart index 61c3694..77404f8 100644 --- a/example/main.dart +++ b/example/main.dart @@ -57,29 +57,4 @@ void main() { .read(json) .map((match) => '${match.path}:\t${match.value}') .forEach(print); - - print('\n'); - - final bikeColor = JsonPath(r'$.store.bicycle.color'); - - print('A copy of the store with repainted bike:'); - print(bikeColor.set(json, 'blue')); - - print('\n'); - - print('Note, that the bike in the original store is still red:'); - bikeColor - .read(json) - .map((result) => '${result.path}:\t${result.value}') - .forEach(print); - - print('\n'); - - print('It is also possible to modify json in place ' - 'as long as the matching value is an object or a list:'); - final someBooks = JsonPath(r'$.store.book[::2]'); - someBooks.read(json).forEach((match) { - match.value['title'] = 'Banana'; - }); - print(json); } diff --git a/lib/json_path.dart b/lib/json_path.dart index a75bf7b..e00dcb6 100644 --- a/lib/json_path.dart +++ b/lib/json_path.dart @@ -1,6 +1,8 @@ /// JSONPath for Dart library json_path; +export 'package:json_path/json_pointer.dart'; +export 'package:json_path/src/filter_not_found.dart'; export 'package:json_path/src/json_path.dart'; export 'package:json_path/src/json_path_match.dart'; -export 'package:json_path/src/predicate.dart'; +export 'package:json_path/src/matching_context.dart'; diff --git a/lib/json_pointer.dart b/lib/json_pointer.dart new file mode 100644 index 0000000..0b62762 --- /dev/null +++ b/lib/json_pointer.dart @@ -0,0 +1,18 @@ +/// JSON Pointer (RFC 6901). +class JsonPointer { + /// Creates a pointer to the root element + const JsonPointer() : value = ''; + + JsonPointer._(this.value); + + /// The string value of the pointer + final String value; + + /// Returns a new instance of [JsonPointer] + /// with the [segment] appended at the end. + JsonPointer append(String segment) => JsonPointer._( + value + '/' + segment.replaceAll('~', '~0').replaceAll('/', '~1')); + + @override + String toString() => value; +} diff --git a/lib/src/any_match.dart b/lib/src/any_match.dart new file mode 100644 index 0000000..fa70950 --- /dev/null +++ b/lib/src/any_match.dart @@ -0,0 +1,30 @@ +import 'package:json_path/json_pointer.dart'; +import 'package:json_path/src/json_path_match.dart'; +import 'package:json_path/src/matching_context.dart'; + +class AnyMatch implements JsonPathMatch { + const AnyMatch( + {required this.value, + required this.path, + required this.pointer, + required this.context, + this.parent}); + + /// The value + @override + final T value; + + /// JSONPath to this match + @override + final String path; + + /// JSON Pointer (RFC 6901) to this match + @override + final JsonPointer pointer; + + @override + final MatchingContext context; + + @override + final JsonPathMatch? parent; +} diff --git a/lib/src/ast/ast.dart b/lib/src/ast/ast.dart deleted file mode 100644 index fcfaf28..0000000 --- a/lib/src/ast/ast.dart +++ /dev/null @@ -1,25 +0,0 @@ -import 'package:json_path/src/ast/node.dart'; - -/// The Abstract Syntax Tree -class AST { - AST(Iterable tokens) { - tokens.skipWhile((_) => _ == r'$').forEach(_processToken); - } - - /// The children of the root node - Iterable get nodes => _stack.last.children; - - final _stack = [Node('')]; - - void _processToken(String token) { - if (token == '[') { - _stack.add(Node(token)); - } else if (token == ']') { - final node = _stack.removeLast(); - if (node.value != '[') throw FormatException('Mismatching brackets'); - _stack.last.children.add(node); - } else { - _stack.last.children.add(Node(token)); - } - } -} diff --git a/lib/src/ast/node.dart b/lib/src/ast/node.dart deleted file mode 100644 index 22f6d12..0000000 --- a/lib/src/ast/node.dart +++ /dev/null @@ -1,19 +0,0 @@ -class Node { - Node(this.value); - - final String value; - final children = []; - - bool get isNumber => RegExp(r'^-?\d+$').hasMatch(value); - - bool get isQuoted => value.startsWith("'") && value.endsWith("'"); - - bool get isWildcard => value == '*'; - - String get unquoted => value - .substring(1, value.length - 1) - .replaceAll(r'\\', r'\') - .replaceAll(r"\'", r"'"); - - int get intValue => int.parse(value); -} diff --git a/lib/src/ast/tokenize.dart b/lib/src/ast/tokenize.dart deleted file mode 100644 index 25e10f2..0000000 --- a/lib/src/ast/tokenize.dart +++ /dev/null @@ -1,17 +0,0 @@ -Iterable tokenize(String expr) => - _tokens.allMatches(expr).map((match) => match.group(0).toString()); - -final _tokens = RegExp([ - r'\$', // root - r'\[', // left bracket - r'\]', // right bracket - r'\.\.', // recursion - r'\.', // child - r'\*', // wildcard - r'\:', // slice - r'\,', // union - r'\?', // filter - r"'(?:[^'\\]|\\.)*'", // quoted string - r'-?\d+', // number - r'([A-Za-z_-][A-Za-z_\d-]*)', // field -].join('|')); diff --git a/lib/src/build_parser.dart b/lib/src/build_parser.dart new file mode 100644 index 0000000..a50e6fb --- /dev/null +++ b/lib/src/build_parser.dart @@ -0,0 +1,7 @@ +import 'package:json_path/src/grammar.dart'; +import 'package:json_path/src/selector/selector.dart'; +import 'package:petitparser/core.dart'; + +Parser buildParser() { + return jsonPath; +} diff --git a/lib/src/filter_not_found.dart b/lib/src/filter_not_found.dart new file mode 100644 index 0000000..9b434a2 --- /dev/null +++ b/lib/src/filter_not_found.dart @@ -0,0 +1,8 @@ +class FilterNotFound implements Exception { + FilterNotFound(this.message); + + final String message; + + @override + String toString() => message; +} diff --git a/lib/src/grammar.dart b/lib/src/grammar.dart new file mode 100644 index 0000000..450bd00 --- /dev/null +++ b/lib/src/grammar.dart @@ -0,0 +1,117 @@ +import 'package:json_path/src/selector/array_index.dart'; +import 'package:json_path/src/selector/array_slice.dart'; +import 'package:json_path/src/selector/field.dart'; +import 'package:json_path/src/selector/named_filter.dart'; +import 'package:json_path/src/selector/recursion.dart'; +import 'package:json_path/src/selector/selector.dart'; +import 'package:json_path/src/selector/sequence.dart'; +import 'package:json_path/src/selector/union.dart'; +import 'package:json_path/src/selector/wildcard.dart'; +import 'package:petitparser/petitparser.dart'; + +final minus = char('-'); +final escape = char(r'\'); +final hexDigit = anyOf('0123456789ABCDEF'); + +final unicodeSymbol = (string(r'\u') & hexDigit.repeat(4).flatten()) + .map((value) => String.fromCharCode(int.parse(value.last, radix: 16))); + +final doubleQuote = char('"'); +final escapedDoubleQuote = (escape & doubleQuote).map((_) => '"'); + +final singleQuote = char("'"); +final escapedSingleQuote = (escape & singleQuote).map((_) => "'"); + +final escapedSlash = string(r'\/').map((_) => r'/'); +final escapedBackSlash = string(r'\\').map((_) => r'\'); +final escapedBackspace = string(r'\b').map((_) => '\b'); +final escapedFormFeed = string(r'\f').map((_) => '\f'); +final escapedNewLine = string(r'\n').map((_) => '\n'); +final escapedReturn = string(r'\r').map((_) => '\r'); +final escapedTab = string(r'\t').map((_) => '\t'); + +final escapedControl = escapedSlash | + escapedBackSlash | + escapedBackspace | + escapedFormFeed | + escapedNewLine | + escapedReturn | + escapedTab; + +final wildcard = char('*').map((_) => const Wildcard()); + +final fieldNameChar = + minus | char('_') | letter() | digit() | range(0x80, 0x10FFF); +final fieldName = fieldNameChar.plus().flatten().map((value) => Field(value)); + +final dotMatcher = + (char('.') & (fieldName | wildcard)).map((value) => value.last); + +final doubleUnescaped = + range(0x20, 0x21) | range(0x23, 0x5B) | range(0x5D, 0x10FFF); + +final doubleInner = + (doubleUnescaped | escapedDoubleQuote | escapedControl | unicodeSymbol) + .star() + .map((value) => value.join('')); + +final doubleQuotedString = + (doubleQuote & doubleInner & doubleQuote).map((value) => Field(value[1])); + +final singleUnescaped = + range(0x20, 0x26) | range(0x28, 0x5B) | range(0x5D, 0x10FFF); + +final singleInner = + (singleUnescaped | escapedSingleQuote | escapedControl | unicodeSymbol) + .star() + .map((value) => value.join('')); + +final singleQuotedString = + (singleQuote & singleInner & singleQuote).map((value) => Field(value[1])); + +final integer = (minus.optional() & digit().plus()).flatten().map(int.parse); + +final colon = char(':').trim(); + +final maybeInteger = integer.optional(); + +final arraySlice = + (maybeInteger & colon & maybeInteger & (colon & maybeInteger).optional()) + .map((value) => + ArraySlice(start: value[0], stop: value[2], step: value[3]?[1])); + +final arrayIndex = integer.map((value) => ArrayIndex(value)); + +final namedFilter = (char('?') & + ((char('_') | letter()) & (char('_') | letter() | digit()).star()) + .flatten()) + .map((value) => NamedFilter(value.last)); + +final unionElement = (arraySlice | + arrayIndex | + wildcard | + singleQuotedString | + doubleQuotedString | + namedFilter) + .trim(); + +final subsequentUnionElement = + (char(',') & unionElement).map((value) => value.last); + +final unionContent = (unionElement & subsequentUnionElement.star()).map( + (value) => [value.first as Selector] + .followedBy((value.last as List).map((v) => v as Selector))); + +final union = + (char('[') & unionContent & char(']')).map((value) => Union(value[1])); + +final recursion = (string('..') & (wildcard | union | fieldName | endOfInput())) + .map((value) => (value.last == null) + ? const Recursion() + : Sequence([const Recursion(), value.last])); + +final selector = dotMatcher | union | recursion; + +final jsonPath = (char(r'$') & selector.star()) + .end() + .map((value) => Sequence((value.last as List).map((e) => e as Selector))); diff --git a/lib/src/json_path.dart b/lib/src/json_path.dart index 9175d81..0a8e9f2 100644 --- a/lib/src/json_path.dart +++ b/lib/src/json_path.dart @@ -1,9 +1,6 @@ -import 'package:json_path/src/ast/ast.dart'; -import 'package:json_path/src/ast/tokenize.dart'; +import 'package:json_path/src/grammar.dart' as grammar; import 'package:json_path/src/json_path_match.dart'; -import 'package:json_path/src/parsing_state.dart'; -import 'package:json_path/src/predicate.dart'; -import 'package:json_path/src/selector/root.dart'; +import 'package:json_path/src/match_factory.dart'; import 'package:json_path/src/selector/selector.dart'; /// A JSONPath expression @@ -11,31 +8,26 @@ class JsonPath { /// Creates an instance from string. The [expression] is parsed once, and /// the instance may be used many times after that. /// - /// The [filter] arguments may contain the named filters used - /// in the [expression]. - /// /// Throws [FormatException] if the [expression] can not be parsed. - factory JsonPath(String expression, - {Map filter = const {}}) { - if (expression.isEmpty) throw FormatException('Empty expression'); - ParsingState state = Ready(RootSelector()); - AST(tokenize(expression)).nodes.forEach((node) { - state = state.process(node, filter); - }); - return JsonPath._(state.selector); - } - - JsonPath._(this._selector); + JsonPath(this.expression, {this.filters = const {}}) + : _selector = grammar.jsonPath.parse(expression).value; + /// JSONPath expression. + final String expression; final Selector _selector; - /// Reads the given [json] object returning an Iterable of all matches found. - Iterable read(json) => - _selector.read([JsonPathMatch(json, '')]); + /// Named callback filters + final Map filters; + + /// Reads the given [document] object returning an Iterable of all matches found. + Iterable read(document, + {Map filters = const {}}) => + _selector + .read(rootMatch(document, expression, {...this.filters, ...filters})); - /// Returns a copy of [json] with all matching values replaced with [value]. - dynamic set(json, value) => _selector.set(json, (_) => value); + /// Reads the given [json] object returning an Iterable of all values found. + Iterable readValues(json) => read(json).map((_) => _.value); @override - String toString() => _selector.expression(); + String toString() => expression; } diff --git a/lib/src/json_path_match.dart b/lib/src/json_path_match.dart index 9d13c4f..990478d 100644 --- a/lib/src/json_path_match.dart +++ b/lib/src/json_path_match.dart @@ -1,10 +1,22 @@ -/// A single matching result -class JsonPathMatch { - JsonPathMatch(this.value, this.path); +import 'package:json_path/json_pointer.dart'; +import 'package:json_path/src/matching_context.dart'; +/// A named filter function +typedef CallbackFilter = bool Function(JsonPathMatch match); + +abstract class JsonPathMatch { /// The value - final T value; + T get value; + + /// JSONPath to this match + String get path; + + /// JSON Pointer (RFC 6901) to this match + JsonPointer get pointer; + + /// Matching context + MatchingContext get context; - /// JSONPath to this result - final String path; + /// The parent match + JsonPathMatch? get parent; } diff --git a/lib/src/match_factory.dart b/lib/src/match_factory.dart new file mode 100644 index 0000000..2e00332 --- /dev/null +++ b/lib/src/match_factory.dart @@ -0,0 +1,92 @@ +import 'package:json_path/json_pointer.dart'; +import 'package:json_path/src/any_match.dart'; +import 'package:json_path/src/json_path_match.dart'; +import 'package:json_path/src/matching_context.dart'; +import 'package:json_path/src/quote.dart'; + +/// Creates a match for the root element +JsonPathMatch rootMatch( + dynamic value, String expression, Map filter) => + _newMatch( + value: value, + path: r'$', + pointer: const JsonPointer(), + context: MatchingContext(expression, filter), + parent: null); + +class ListMatch extends AnyMatch { + const ListMatch( + {required List value, + required String path, + required JsonPointer pointer, + required MatchingContext context, + JsonPathMatch? parent}) + : super( + value: value, + path: path, + pointer: pointer, + context: context, + parent: parent); + + /// Creates a match for the child element. + JsonPathMatch child(int key) => _newMatch( + value: value[key], + path: path + '[' + key.toString() + ']', + pointer: pointer.append(key.toString()), + context: context, + parent: this); +} + +class MapMatch extends AnyMatch { + const MapMatch( + {required Map value, + required String path, + required JsonPointer pointer, + required MatchingContext context, + JsonPathMatch? parent}) + : super( + value: value, + path: path, + pointer: pointer, + context: context, + parent: parent); + + /// Creates a match for the child element. + JsonPathMatch child(String key) => _newMatch( + value: value[key], + path: path + '[' + quote(key) + ']', + pointer: pointer.append(key), + context: context, + parent: this); +} + +/// Creates a new match depending on the value type +JsonPathMatch _newMatch( + {required dynamic value, + required String path, + required JsonPointer pointer, + required MatchingContext context, + required JsonPathMatch? parent}) { + if (value is List) { + return ListMatch( + value: value, + path: path, + pointer: pointer, + context: context, + parent: parent); + } + if (value is Map) { + return MapMatch( + value: value, + path: path, + pointer: pointer, + context: context, + parent: parent); + } + return AnyMatch( + value: value, + path: path, + pointer: pointer, + context: context, + parent: parent); +} diff --git a/lib/src/matching_context.dart b/lib/src/matching_context.dart new file mode 100644 index 0000000..3a2096c --- /dev/null +++ b/lib/src/matching_context.dart @@ -0,0 +1,11 @@ +import 'package:json_path/src/json_path_match.dart'; + +class MatchingContext { + const MatchingContext(this.expression, this.filters); + + /// JSON Path expression + final String expression; + + /// Named callback filters + final Map filters; +} diff --git a/lib/src/parsing_state.dart b/lib/src/parsing_state.dart deleted file mode 100644 index af794bf..0000000 --- a/lib/src/parsing_state.dart +++ /dev/null @@ -1,123 +0,0 @@ -import 'package:json_path/src/ast/node.dart'; -import 'package:json_path/src/predicate.dart'; -import 'package:json_path/src/selector/filter.dart'; -import 'package:json_path/src/selector/list_union.dart'; -import 'package:json_path/src/selector/list_wildcard.dart'; -import 'package:json_path/src/selector/object_union.dart'; -import 'package:json_path/src/selector/object_wildcard.dart'; -import 'package:json_path/src/selector/recursive.dart'; -import 'package:json_path/src/selector/selector.dart'; -import 'package:json_path/src/selector/slice.dart'; - -/// AST parser state -abstract class ParsingState { - /// Processes the node. Returns the next state - ParsingState process(Node node, Map filters); - - /// Selector made from the tree - Selector get selector; -} - -/// Ready to process the next node -class Ready implements ParsingState { - const Ready(this.selector); - - @override - final Selector selector; - - @override - ParsingState process(Node node, Map filters) { - switch (node.value) { - case '[': - return Ready(selector.then(_brackets(node.children, filters))); - case '.': - return AwaitingField(selector); - case '..': - return Ready(selector.then(Recursive())); - case '*': - return Ready(selector.then(ObjectWildcard())); - default: - return Ready(selector.then(ObjectUnion([node.value]))); - } - } - - Selector _brackets(List nodes, Map filters) { - if (nodes.isEmpty) throw FormatException('Empty brackets'); - if (nodes.length == 1) return _singleValueBrackets(nodes.single); - return _multiValueBrackets(nodes, filters); - } - - Selector _singleValueBrackets(Node node) { - if (node.isWildcard) return ListWildcard(); - if (node.isNumber) return ListUnion([node.intValue]); - if (node.isQuoted) return ObjectUnion([node.unquoted]); - throw FormatException('Unexpected bracket expression'); - } - - Selector _multiValueBrackets( - List nodes, Map filters) { - if (_isFilter(nodes)) return _filter(nodes, filters); - if (_isSlice(nodes)) return _slice(nodes); - return _union(nodes); - } - - Filter _filter(List nodes, Map filters) { - final name = nodes[1].value; - final filter = filters[name]; - if (filter == null) { - throw FormatException('Filter not found: "${name}"'); - } - return Filter(name, filter); - } - - bool _isFilter(List nodes) => nodes.first.value == '?'; - - bool _isSlice(List nodes) => nodes.any((node) => node.value == ':'); - - Selector _union(List nodes) { - final filtered = nodes.where((_) => _.value != ','); - if (filtered.every((_) => _.isNumber)) { - return ListUnion(filtered.map((_) => _.intValue).toList()); - } - return ObjectUnion( - filtered.map((_) => _.isQuoted ? _.unquoted : _.value).toList()); - } - - Slice _slice(List nodes) { - int? first; - int? last; - int? step; - var colons = 0; - nodes.forEach((node) { - if (node.value == ':') { - colons++; - return; - } - if (colons == 0) { - first = node.intValue; - return; - } - if (colons == 1) { - last = node.intValue; - return; - } - step = node.intValue; - }); - return Slice(first: first, last: last, step: step); - } -} - -class AwaitingField implements ParsingState { - const AwaitingField(this.selector); - - @override - final Selector selector; - - @override - ParsingState process(Node node, Map filters) { - if (node.isWildcard) { - return Ready(selector.then(ObjectWildcard())); - } - return Ready(selector.then(ObjectUnion([node.value]))); - } -} diff --git a/lib/src/predicate.dart b/lib/src/predicate.dart deleted file mode 100644 index 552da30..0000000 --- a/lib/src/predicate.dart +++ /dev/null @@ -1 +0,0 @@ -typedef Predicate = bool Function(dynamic element); diff --git a/lib/src/quote.dart b/lib/src/quote.dart index e74ca1d..dfcefdf 100644 --- a/lib/src/quote.dart +++ b/lib/src/quote.dart @@ -1,14 +1,13 @@ -/// A single-quoted string. -/// Example: -/// `hello` => `'hello'` -/// `i'm \ok` => `'i\'m \\ok'` -class Quote { - Quote(String value) - : value = - "'" + value.replaceAll(r'\', r'\\').replaceAll("'", r"\'") + "'"; - - final String value; - - @override - String toString() => value; -} +/// Quotes a [string] using [quotationMark] +String quote(String string, {String quotationMark = "'"}) => + quotationMark + + string + .replaceAll(r'/', r'\/') + .replaceAll(r'\', r'\\') + .replaceAll('\b', r'\b') + .replaceAll('\f', r'\f') + .replaceAll('\n', r'\n') + .replaceAll('\r', r'\r') + .replaceAll('\t', r'\t') + .replaceAll(quotationMark, r'\' + quotationMark) + + quotationMark; diff --git a/lib/src/selector/array_index.dart b/lib/src/selector/array_index.dart new file mode 100644 index 0000000..26389c3 --- /dev/null +++ b/lib/src/selector/array_index.dart @@ -0,0 +1,19 @@ +import 'package:json_path/src/json_path_match.dart'; +import 'package:json_path/src/match_factory.dart'; +import 'package:json_path/src/selector/selector.dart'; + +class ArrayIndex implements Selector { + ArrayIndex(this.index); + + final int index; + + @override + Iterable read(JsonPathMatch match) sync* { + if (match is ListMatch) { + final normalizedIndex = index < 0 ? match.value.length + index : index; + if (normalizedIndex >= 0 && normalizedIndex < match.value.length) { + yield match.child(normalizedIndex); + } + } + } +} diff --git a/lib/src/selector/array_slice.dart b/lib/src/selector/array_slice.dart new file mode 100644 index 0000000..e072261 --- /dev/null +++ b/lib/src/selector/array_slice.dart @@ -0,0 +1,57 @@ +import 'dart:math'; + +import 'package:json_path/src/json_path_match.dart'; +import 'package:json_path/src/match_factory.dart'; +import 'package:json_path/src/selector/selector.dart'; + +class ArraySlice implements Selector { + ArraySlice({this.start, this.stop, int? step}) : step = step ?? 1; + + final int? start; + final int? stop; + final int step; + + @override + Iterable read(JsonPathMatch match) sync* { + if (match is ListMatch) { + yield* _iterate(match.value).map((i) => match.child(i)); + } + } + + Iterable _iterate(List list) sync* { + if (step > 0) yield* _iterateForward(list); + if (step < 0) yield* _iterateBackward(list); + } + + Iterable _iterateForward(List list) sync* { + final stop = this.stop ?? list.length; + final start = this.start ?? 0; + final low = start < 0 ? max(list.length + start, 0) : start; + final high = stop < 0 ? list.length + stop : min(list.length, stop); + for (var i = low; i < high; i += step) { + yield i; + } + } + + Iterable _iterateBackward(List list) sync* { + final low = _low(stop, list.length); + final high = _high(start, list.length); + for (var i = high; i > low; i += step) { + yield i; + } + } + + /// exclusive + int _low(int? stop, int length) { + if (stop == null) return -1; + if (stop < 0) return max(length + stop, -1); + return stop; + } + + /// inclusive + int _high(int? start, int length) { + if (start == null) return length - 1; + if (start < 0) return length + start; + return min(start, length - 1); + } +} diff --git a/lib/src/selector/field.dart b/lib/src/selector/field.dart new file mode 100644 index 0000000..9fb4286 --- /dev/null +++ b/lib/src/selector/field.dart @@ -0,0 +1,16 @@ +import 'package:json_path/src/json_path_match.dart'; +import 'package:json_path/src/match_factory.dart'; +import 'package:json_path/src/selector/selector.dart'; + +class Field implements Selector { + Field(this.name); + + final String name; + + @override + Iterable read(JsonPathMatch match) sync* { + if (match is MapMatch && match.value.containsKey(name)) { + yield match.child(name); + } + } +} diff --git a/lib/src/selector/filter.dart b/lib/src/selector/filter.dart deleted file mode 100644 index b6671b6..0000000 --- a/lib/src/selector/filter.dart +++ /dev/null @@ -1,24 +0,0 @@ -import 'package:json_path/json_path.dart'; -import 'package:json_path/src/json_path_match.dart'; -import 'package:json_path/src/predicate.dart'; -import 'package:json_path/src/selector/selector.dart'; -import 'package:json_path/src/selector/selector_mixin.dart'; - -class Filter with SelectorMixin implements Selector { - Filter(this.name, this.isApplicable); - - final String name; - - final Predicate isApplicable; - - @override - Iterable read(Iterable matches) => - matches.where((r) => isApplicable(r.value)); - - @override - String expression() => '[?$name]'; - - @override - dynamic set(json, Replacement replacement) => - isApplicable(json) ? replacement(json) : json; -} diff --git a/lib/src/selector/joint.dart b/lib/src/selector/joint.dart deleted file mode 100644 index 427c400..0000000 --- a/lib/src/selector/joint.dart +++ /dev/null @@ -1,39 +0,0 @@ -import 'package:json_path/src/json_path_match.dart'; -import 'package:json_path/src/selector/object_wildcard.dart'; -import 'package:json_path/src/selector/recursive.dart'; -import 'package:json_path/src/selector/selector.dart'; -import 'package:json_path/src/selector/selector_mixin.dart'; - -/// A combination of two adjacent selectors -class Joint with SelectorMixin implements Selector { - Joint(this.left, this.right); - - /// Rightmost leaf in the tree - static Selector rightmost(Selector s) => s is Joint ? rightmost(s.right) : s; - - /// Leftmost leaf in the tree - static Selector leftmost(Selector s) => s is Joint ? leftmost(s.left) : s; - - static String delimiter(Selector left, Selector right) => - (left is! Recursive && right is ObjectWildcard) ? '.' : ''; - - /// Left subtree - final Selector left; - - /// Right subtree - final Selector right; - - @override - Iterable read(Iterable matches) => - right.read(left.read(matches)); - - @override - String expression() => - left.expression() + - delimiter(rightmost(left), leftmost(right)) + - right.expression(); - - @override - dynamic set(json, Replacement replacement) => - left.set(json, (_) => right.set(_, replacement)); -} diff --git a/lib/src/selector/list_union.dart b/lib/src/selector/list_union.dart deleted file mode 100644 index af3664c..0000000 --- a/lib/src/selector/list_union.dart +++ /dev/null @@ -1,43 +0,0 @@ -import 'package:json_path/src/json_path_match.dart'; -import 'package:json_path/src/selector/selector.dart'; -import 'package:json_path/src/selector/selector_mixin.dart'; - -class ListUnion with SelectorMixin implements Selector { - ListUnion(List keys) : _indices = keys.toSet().toList()..sort(); - - final List _indices; - - @override - Iterable read(Iterable matches) => matches - .map((r) => (r.value is List) ? _map(r.value, r.path) : []) - .expand((_) => _); - - @override - String expression() => '[${_indices.join(',')}]'; - - @override - dynamic set(json, Replacement replacement) { - json ??= []; - if (json is List) { - json = [...json]; - _replaceInList(json, replacement); - } - return json; - } - - Iterable _map(List list, String path) => _indices - .where((key) => key < list.length) - .map((key) => JsonPathMatch(list[key], path + '[$key]')); - - void _replaceInList(List list, Replacement replacement) => - _indices.forEach((index) { - if (index < list.length) { - list[index] = replacement(list[index]); - } else if (index == list.length) { - list.add(replacement(null)); - } else { - throw RangeError( - 'Can not set index $index. Preceding index is missing.'); - } - }); -} diff --git a/lib/src/selector/list_wildcard.dart b/lib/src/selector/list_wildcard.dart deleted file mode 100644 index 9abc2c4..0000000 --- a/lib/src/selector/list_wildcard.dart +++ /dev/null @@ -1,24 +0,0 @@ -import 'package:json_path/src/json_path_match.dart'; -import 'package:json_path/src/selector/selector.dart'; -import 'package:json_path/src/selector/selector_mixin.dart'; - -class ListWildcard with SelectorMixin implements Selector { - @override - Iterable read(Iterable matches) => matches - .where((r) => r.value is List) - .map((r) => _wrap(r.value, r.path)) - .expand((_) => _); - - @override - String expression() => '[*]'; - - @override - dynamic set(json, Replacement replacement) => - (json is List) ? json.map(replacement).toList() : json; - - static Iterable _wrap(List val, String path) sync* { - for (var i = 0; i < val.length; i++) { - yield JsonPathMatch(val[i], path + '[$i]'); - } - } -} diff --git a/lib/src/selector/named_filter.dart b/lib/src/selector/named_filter.dart new file mode 100644 index 0000000..585b683 --- /dev/null +++ b/lib/src/selector/named_filter.dart @@ -0,0 +1,18 @@ +import 'package:json_path/src/filter_not_found.dart'; +import 'package:json_path/src/json_path_match.dart'; +import 'package:json_path/src/selector/selector.dart'; + +class NamedFilter implements Selector { + NamedFilter(this.name); + + final String name; + + @override + Iterable read(JsonPathMatch match) sync* { + final filter = match.context.filters[name]; + if (filter == null) { + throw FilterNotFound('Callback filter "$name" not found'); + } + if (filter(match)) yield match; + } +} diff --git a/lib/src/selector/object_union.dart b/lib/src/selector/object_union.dart deleted file mode 100644 index 32f91a1..0000000 --- a/lib/src/selector/object_union.dart +++ /dev/null @@ -1,33 +0,0 @@ -import 'package:json_path/src/json_path_match.dart'; -import 'package:json_path/src/quote.dart'; -import 'package:json_path/src/selector/selector.dart'; -import 'package:json_path/src/selector/selector_mixin.dart'; - -class ObjectUnion with SelectorMixin implements Selector { - ObjectUnion(List keys) : _keys = keys.toSet().toList(); - - final List _keys; - - @override - Iterable read(Iterable matches) => matches - .map((r) => - (r.value is Map) ? _readMap(r.value, r.path) : []) - .expand((_) => _); - - @override - String expression() => '[${_keys.map((k) => Quote(k)).join(',')}]'; - - @override - dynamic set(json, Replacement replacement) { - if (json == null) return _patch({}, replacement); - if (json is Map) return {...json, ..._patch(json, replacement)}; - return json; - } - - Iterable _readMap(Map map, String path) => _keys - .where(map.containsKey) - .map((key) => JsonPathMatch(map[key], path + '[${Quote(key)}]')); - - Map _patch(Map map, Replacement replacement) => - Map.fromEntries(_keys.map((key) => MapEntry(key, replacement(map[key])))); -} diff --git a/lib/src/selector/object_wildcard.dart b/lib/src/selector/object_wildcard.dart deleted file mode 100644 index 08eb1a8..0000000 --- a/lib/src/selector/object_wildcard.dart +++ /dev/null @@ -1,30 +0,0 @@ -import 'package:json_path/src/json_path_match.dart'; -import 'package:json_path/src/quote.dart'; -import 'package:json_path/src/selector/selector.dart'; -import 'package:json_path/src/selector/selector_mixin.dart'; - -class ObjectWildcard with SelectorMixin implements Selector { - @override - Iterable read(Iterable matches) => - matches.map((r) { - if (r.value is Map) return _allProperties(r.value, r.path); - if (r.value is List) return _allValues(r.value, r.path); - return []; - }).expand((_) => _); - - @override - String expression() => '*'; - - Iterable _allProperties(Map map, String path) => map.entries - .map((e) => JsonPathMatch(e.value, path + '[${Quote(e.key)}]')); - - Iterable _allValues(List list, String path) => list - .asMap() - .entries - .map((e) => JsonPathMatch(e.value, path + '[${e.key}]')); - - @override - dynamic set(json, Replacement replacement) => (json is Map) - ? json.map((key, value) => MapEntry(key, replacement(value))) - : json; -} diff --git a/lib/src/selector/recursion.dart b/lib/src/selector/recursion.dart new file mode 100644 index 0000000..63be858 --- /dev/null +++ b/lib/src/selector/recursion.dart @@ -0,0 +1,17 @@ +import 'package:json_path/src/json_path_match.dart'; +import 'package:json_path/src/selector/selector.dart'; +import 'package:json_path/src/selector/wildcard.dart'; + +class Recursion implements Selector { + const Recursion(); + + @override + Iterable read(JsonPathMatch match) sync* { + yield match; + yield* const Wildcard() + .read(match) + .where((e) => e.value is Map || e.value is List) + .map(read) + .expand((_) => _); + } +} diff --git a/lib/src/selector/recursive.dart b/lib/src/selector/recursive.dart deleted file mode 100644 index d221576..0000000 --- a/lib/src/selector/recursive.dart +++ /dev/null @@ -1,48 +0,0 @@ -import 'package:json_path/src/json_path_match.dart'; -import 'package:json_path/src/quote.dart'; -import 'package:json_path/src/selector/selector.dart'; -import 'package:json_path/src/selector/selector_mixin.dart'; - -class Recursive with SelectorMixin implements Selector { - @override - Iterable read(Iterable matches) => - matches.map(_traverse).expand((_) => _); - - @override - String expression() => '..'; - - @override - dynamic set(json, Replacement replacement) { - if (json is Map) { - return _replaceInMap(json, replacement); - } - if (json is List) { - return _replaceInList(json, replacement); - } - return replacement(json); - } - - Iterable _traverse(JsonPathMatch m) { - if (m.value is Map) { - return [m].followedBy(read(_props(m.value, m.path))); - } - if (m.value is List) { - return [m].followedBy(read(_values(m.value, m.path))); - } - return []; - } - - dynamic _replaceInMap(Map map, Replacement replacement) => replacement( - map.map((key, value) => MapEntry(key, set(value, replacement)))); - - dynamic _replaceInList(List list, Replacement replacement) => - replacement(list.map((value) => set(value, replacement)).toList()); - - Iterable _values(List val, String path) => val - .asMap() - .entries - .map((e) => JsonPathMatch(e.value, path + '[${e.key}]')); - - Iterable _props(Map map, String path) => map.entries - .map((e) => JsonPathMatch(e.value, path + '[${Quote(e.key)}]')); -} diff --git a/lib/src/selector/root.dart b/lib/src/selector/root.dart deleted file mode 100644 index 683db03..0000000 --- a/lib/src/selector/root.dart +++ /dev/null @@ -1,17 +0,0 @@ -import 'package:json_path/src/json_path_match.dart'; -import 'package:json_path/src/selector/selector.dart'; -import 'package:json_path/src/selector/selector_mixin.dart'; - -class RootSelector with SelectorMixin implements Selector { - const RootSelector(); - - @override - Iterable read(Iterable matches) => - matches.map((m) => JsonPathMatch(m.value, expression())); - - @override - String expression() => r'$'; - - @override - dynamic set(json, Replacement replacement) => replacement(json); -} diff --git a/lib/src/selector/selector.dart b/lib/src/selector/selector.dart index ec74a15..c05f0b4 100644 --- a/lib/src/selector/selector.dart +++ b/lib/src/selector/selector.dart @@ -1,18 +1,6 @@ import 'package:json_path/src/json_path_match.dart'; -/// Converts a set of matches into a set of matches abstract class Selector { /// Applies this filter to the [matches] - Iterable read(Iterable matches); - - /// The filter expression as string. - String expression(); - - /// Combines this expression with the [other] - Selector then(Selector other); - - /// Returns a copy of [json] with all selected values modified using [replacement] function. - dynamic set(json, Replacement replacement); + Iterable read(JsonPathMatch match); } - -typedef Replacement = V Function(T value); diff --git a/lib/src/selector/selector_mixin.dart b/lib/src/selector/selector_mixin.dart deleted file mode 100644 index 65256d2..0000000 --- a/lib/src/selector/selector_mixin.dart +++ /dev/null @@ -1,8 +0,0 @@ -import 'package:json_path/src/selector/joint.dart'; -import 'package:json_path/src/selector/selector.dart'; - -mixin SelectorMixin implements Selector { - /// Combines this expression with the [other] - @override - Selector then(Selector other) => Joint(this, other); -} diff --git a/lib/src/selector/sequence.dart b/lib/src/selector/sequence.dart new file mode 100644 index 0000000..858977f --- /dev/null +++ b/lib/src/selector/sequence.dart @@ -0,0 +1,18 @@ +import 'package:json_path/src/json_path_match.dart'; +import 'package:json_path/src/selector/selector.dart'; + +typedef Filter = Iterable Function( + Iterable matches); + +class Sequence implements Selector { + Sequence(Iterable selectors) + : _filter = selectors.fold( + (_) => _, + (filter, selector) => (matches) => + filter(matches).map(selector.read).expand((_) => _)); + + final Filter _filter; + + @override + Iterable read(JsonPathMatch match) => _filter([match]); +} diff --git a/lib/src/selector/slice.dart b/lib/src/selector/slice.dart deleted file mode 100644 index ed47c4f..0000000 --- a/lib/src/selector/slice.dart +++ /dev/null @@ -1,63 +0,0 @@ -import 'dart:math'; - -import 'package:json_path/src/json_path_match.dart'; -import 'package:json_path/src/selector/selector.dart'; -import 'package:json_path/src/selector/selector_mixin.dart'; - -class Slice with SelectorMixin implements Selector { - Slice({int? first, this.last, int? step}) - : first = first ?? 0, - step = step ?? 1; - - static Iterable _for(int from, int to, int step) sync* { - if (step < 1) return; - for (var i = from; i < to; i += step) { - yield i; - } - } - - final int first; - - final int? last; - - final int step; - - @override - Iterable read(Iterable matches) => matches - .map((r) => - (r.value is List) ? _filterList(r.value, r.path) : []) - .expand((_) => _); - - @override - String expression() => - '[${first == 0 ? '' : first}:${last ?? ''}${step != 1 ? ':$step' : ''}]'; - - @override - dynamic set(json, Replacement replacement) { - if (json is List) { - final indices = _indices(json); - if (indices.isNotEmpty) { - final copy = [...json]; - indices.forEach((i) { - copy[i] = replacement(json[i]); - }); - return copy; - } - } - return json; - } - - Iterable _filterList(List list, String path) => - _indices(list).map((i) => JsonPathMatch(list[i], path + '[$i]')); - - Iterable _indices(List list) => - _for(_actualFirst(list.length), _actualLast(list.length), step); - - int _actualFirst(int len) => max(0, first < 0 ? (len + first) : first); - - int _actualLast(int len) { - if (last == null) return len; - if (last! < 0) return min(len, len + last!); - return min(len, last!); - } -} diff --git a/lib/src/selector/union.dart b/lib/src/selector/union.dart new file mode 100644 index 0000000..f1cb550 --- /dev/null +++ b/lib/src/selector/union.dart @@ -0,0 +1,12 @@ +import 'package:json_path/src/json_path_match.dart'; +import 'package:json_path/src/selector/selector.dart'; + +class Union implements Selector { + const Union(this._elements); + + final Iterable _elements; + + @override + Iterable read(JsonPathMatch match) => + _elements.map((e) => e.read(match)).expand((_) => _); +} diff --git a/lib/src/selector/wildcard.dart b/lib/src/selector/wildcard.dart new file mode 100644 index 0000000..0bea38f --- /dev/null +++ b/lib/src/selector/wildcard.dart @@ -0,0 +1,17 @@ +import 'package:json_path/src/json_path_match.dart'; +import 'package:json_path/src/match_factory.dart'; +import 'package:json_path/src/selector/selector.dart'; + +class Wildcard implements Selector { + const Wildcard(); + + @override + Iterable read(JsonPathMatch match) sync* { + if (match is MapMatch) { + yield* match.value.entries.map((e) => match.child(e.key)); + } + if (match is ListMatch) { + yield* match.value.asMap().entries.map((e) => match.child(e.key)); + } + } +} diff --git a/pubspec.yaml b/pubspec.yaml index d60ee92..363cf33 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,13 +1,18 @@ name: json_path -version: 0.3.0-nullsafety +version: 0.3.0-nullsafety.1 description: Implementation of JSONPath expressions like "$.store.book[2].price". Can read and set values in parsed JSON objects. homepage: "https://github.com/f3ath/jessie" environment: sdk: '>=2.12.0-29 <3.0.0' +dependencies: + petitparser: ^4.0.0-nullsafety + dev_dependencies: pedantic: ^1.10.0-nullsafety test: ^1.16.0-nullsafety test_coverage: ^0.5.0 + path: ^1.8.0-nullsafety + yaml: ^3.0.0-nullsafety diff --git a/test/cases/basic.json b/test/cases/basic.json new file mode 100644 index 0000000..2878945 --- /dev/null +++ b/test/cases/basic.json @@ -0,0 +1,172 @@ +{ + "tests": [ + { + "name": "root", + "selector": "$", + "document": ["first", "second"], + "values": [["first", "second"]], + "paths": ["$"], + "pointers": [""] + }, { + "name": "dot field on object", + "selector": "$.a", + "document": {"a": "A", "b": "B"}, + "values": ["A"], + "paths": ["$['a']"], + "pointers": ["/a"] + }, { + "name": "dot field on array", + "selector": "$.a", + "document": ["A", "B"], + "values": [] + }, { + "name": "dot wildcard on object", + "selector": "$.*", + "document": {"a": "A", "b": "B"}, + "values": ["A", "B"], + "paths": ["$['a']", "$['b']"], + "pointers": ["/a", "/b"] + }, { + "name": "union wildcard on object", + "selector": "$[*]", + "document": {"a": "A", "b": "B"}, + "values": ["A", "B"], + "paths": ["$['a']", "$['b']"], + "pointers": ["/a", "/b"] + }, { + "name": "dot wildcard on array", + "selector": "$.*", + "document": ["A", "B"], + "values": ["A", "B"], + "paths": ["$[0]", "$[1]"], + "pointers": ["/0", "/1"] + }, { + "name": "dot wildcard dot field", + "selector": "$.*.a", + "document": {"x": {"a": "Ax", "b": "Bx"}, "y": {"a": "Ay", "b": "By"}}, + "values": ["Ax", "Ay"], + "paths": ["$['x']['a']", "$['y']['a']"], + "pointers": ["/x/a", "/y/a"] + }, { + "name": "union sq field on object", + "selector": "$['a']", + "document": {"a": "A", "b": "B"}, + "values": ["A"], + "paths": ["$['a']"], + "pointers": ["/a"] + }, { + "name": "union sq field on object (2 values)", + "selector": "$['a', 'c']", + "document": {"a": "A", "b": "B", "c": "C"}, + "values": ["A", "C"], + "paths": ["$['a']", "$['c']"], + "pointers": ["/a", "/c"] + }, { + "name": "union sq field on object (numeric key)", + "selector": "$['1']", + "document": {"0": "A", "1": "B"}, + "values": ["B"], + "paths": ["$['1']"], + "pointers": ["/1"] + }, { + "name": "union sq field on array", + "selector": "$['a']", + "document": ["A", "B"], + "values": [] + }, { + "name": "union dq field on object", + "selector": "$[\"a\"]", + "document": {"a": "A", "b": "B"}, + "values": ["A"], + "paths": ["$['a']"], + "pointers": ["/a"] + }, { + "name": "union dq field on object (2 values)", + "selector": "$[\"a\", \"c\"]", + "document": {"a": "A", "b": "B", "c": "C"}, + "values": ["A", "C"], + "paths": ["$['a']", "$['c']"], + "pointers": ["/a", "/c"] + }, { + "name": "union dq field on object (numeric key)", + "selector": "$[\"1\"]", + "document": {"0": "A", "1": "B"}, + "values": ["B"], + "paths": ["$['1']"], + "pointers": ["/1"] + }, { + "name": "union dq field on array", + "selector": "$[\"a\"]", + "document": ["A", "B"], + "values": [] + }, { + "name": "union index on array", + "selector": "$[1]", + "document": ["A", "B"], + "values": ["B"], + "paths": ["$[1]"], + "pointers": ["/1"] + }, { + "name": "union index on object", + "selector": "$[1]", + "document": {"a": "A", "b": "B"}, + "values": [] + }, { + "name": "union index and field on object", + "selector": "$[\"0\", 1, 'a']", + "document": {"a": "A", "b": "B", "0": "Zero", "1": "One"}, + "values": ["Zero", "A"], + "paths": ["$['0']", "$['a']"], + "pointers": ["/0", "/a"] + }, { + "name": "union index and field on array", + "selector": "$[1, 'a', '1']", + "document": ["A", "B"], + "values": ["B"], + "paths": ["$[1]"], + "pointers": ["/1"] + }, { + "name": "union array slice on array [15:-7:-3]", + "selector": "$[15:-7:-3]", + "document": [0, 1, 2, 3, 4, 5, 6, 7, 8, 9], + "values": [9, 6], + "paths": ["$[9]", "$[6]"], + "pointers": ["/9", "/6"] + }, { + "name": "union array slice on array [:]", + "selector": "$[:]", + "document": [0, 1, 2], + "values": [0, 1, 2], + "paths": ["$[0]", "$[1]", "$[2]"], + "pointers": ["/0", "/1", "/2"] + }, { + "name": "union array slice on array [::]", + "selector": "$[:10000000000:]", + "document": [0, 1, 2], + "values": [0, 1, 2], + "paths": ["$[0]", "$[1]", "$[2]"], + "pointers": ["/0", "/1", "/2"] + }, { + "name": "recursion", + "selector": "$..", + "document": {"a": {"foo": "bar"}, "b": [42]}, + "values": [{"a": {"foo": "bar"}, "b": [42]}, {"foo": "bar"}, [42]], + "paths": ["$", "$['a']", "$['b']"], + "pointers": ["", "/a", "/b"] + }, { + "name": "recursion wildcard", + "selector": "$..*", + "document": {"a": {"foo": "bar"}, "b": [42]}, + "values": [{"foo": "bar"}, [42], "bar", 42], + "paths": ["$['a']", "$['b']", "$['a']['foo']", "$['b'][0]"], + "pointers": ["/a", "/b", "/a/foo", "/b/0"] + }, { + "name": "recursion union", + "selector": "$..[0]", + "document": {"a": {"foo": "bar"}, "b": [42]}, + "values": [42], + "paths": ["$['b'][0]"], + "pointers": ["/b/0"] + } + ] +} \ No newline at end of file diff --git a/test/cases/unicode.json b/test/cases/unicode.json new file mode 100644 index 0000000..a5e80a6 --- /dev/null +++ b/test/cases/unicode.json @@ -0,0 +1,10 @@ +{ + "tests": [ + { + "name": "dot field, ☺ name", + "selector": "$.☺", + "document": {"☺": "A", "b": "B"}, + "values": ["A"] + } + ] +} \ No newline at end of file diff --git a/test/cases_test.dart b/test/cases_test.dart new file mode 100644 index 0000000..fa0dfd5 --- /dev/null +++ b/test/cases_test.dart @@ -0,0 +1,65 @@ +import 'dart:convert'; +import 'dart:io'; + +import 'package:json_path/json_path.dart'; +import 'package:path/path.dart' as path; +import 'package:test/test.dart'; + +void main() { + const allowedFields = { + 'name', + 'selector', + 'document', + 'values', + 'paths', + 'pointers' + }; + Directory('test/cases') + .listSync() + .whereType() + .where((file) => file.path.endsWith('.json')) + .forEach((file) { + group(path.basenameWithoutExtension(file.path), () { + final cases = jsonDecode(file.readAsStringSync()); + (cases['tests'] as List).forEach((t) { + (t as Map).keys.forEach((key) { + if (!allowedFields.contains(key)) { + throw 'Invalid key "$key"'; + } + }); + + final name = t['name']; + final values = t['values']; + final paths = t['paths']; + final pointers = t['pointers']; + final jp = JsonPath(t['selector']); + group(name, () { + if (values is List) { + test('values', () { + expect(jp.readValues(t['document']), equals(values)); + }); + } + if (paths is List) { + test('paths', () { + expect(jp.read(t['document']).map((e) => e.path).toList(), + equals(paths)); + }); + } + if (pointers is List) { + test('pointers', () { + expect( + jp + .read(t['document']) + .map((e) => e.pointer.toString()) + .toList(), + equals(pointers)); + }); + } + if ([values, paths, pointers].every((_) => _ == null)) { + throw 'No expectations found'; + } + }); + }); + }); + }); +} diff --git a/test/cts_test.dart b/test/cts_test.dart new file mode 100644 index 0000000..a443f9f --- /dev/null +++ b/test/cts_test.dart @@ -0,0 +1,31 @@ +import 'dart:convert'; +import 'dart:io'; + +import 'package:json_path/json_path.dart'; +import 'package:test/test.dart'; + +void main() { + final suite = jsonDecode(File('cts/cts.json').readAsStringSync()); + final tests = suite['tests'] as List; + group('JSON Path Compliance Suite', () { + tests.forEach((t) { + final String name = t['name']; + final String selector = t['selector']; + final document = t['document']; + final List? result = t['result']; + final bool invalid = t['invalid_selector'] ?? false; + test(name, () { + if (invalid) { + try { + JsonPath(selector); + // allows us to be less strict than CTS + } on FormatException { + // do nothing as CTS expects us to throw + } + } else { + expect(JsonPath(selector).readValues(document), equals(result!)); + } + }); + }); + }); +} diff --git a/test/filter_test.dart b/test/filter_test.dart deleted file mode 100644 index 739c3e2..0000000 --- a/test/filter_test.dart +++ /dev/null @@ -1,339 +0,0 @@ -import 'dart:convert'; -import 'dart:io'; - -import 'package:json_path/json_path.dart'; -import 'package:test/test.dart'; - -void main() { - final json = jsonDecode(File('test/store.json').readAsStringSync()); - group('Basic expressions', () { - test('Only root', () { - final root = JsonPath(r'$'); - expect(root.toString(), r'$'); - expect(root.read(json).single.value, json); - expect(root.read(json).single.path, r'$'); - }); - - test('Single field', () { - final store = JsonPath(r'$.store'); - expect(store.toString(), r"$['store']"); - expect(store.read(json).single.value, json['store']); - expect(store.read(json).single.path, r"$['store']"); - }); - - test('Single field with digits', () { - final j = {'aA_12': 'foo'}; - final store = JsonPath(r'$aA_12'); - expect(store.toString(), r"$['aA_12']"); - expect(store.read(j).single.value, 'foo'); - expect(store.read(j).single.path, r"$['aA_12']"); - }); - - test('Single field in bracket notation', () { - final store = JsonPath(r"$['store']"); - expect(store.toString(), r"$['store']"); - expect(store.read(json).single.value, json['store']); - expect(store.read(json).single.path, r"$['store']"); - }); - - test('Mixed brackets and fields', () { - final price = JsonPath(r"$['store'].bicycle['price']"); - expect(price.toString(), r"$['store']['bicycle']['price']"); - expect(price.read(json).single.value, json['store']['bicycle']['price']); - expect(price.read(json).single.path, r"$['store']['bicycle']['price']"); - }); - }); - - group('Invalid format', () { - test('Empty', () { - expect(() => JsonPath(''), throwsFormatException); - }); - }); - - group('Slices', () { - final abc = 'abcdefg'.split(''); - test('1:3', () { - final slice = JsonPath(r'$[1:3]'); - expect(slice.toString(), r'$[1:3]'); - expect(slice.read(abc).length, 2); - expect(slice.read(abc).first.value, 'b'); - expect(slice.read(abc).first.path, r'$[1]'); - expect(slice.read(abc).last.value, 'c'); - expect(slice.read(abc).last.path, r'$[2]'); - }); - test('1:5:2', () { - final slice = JsonPath(r'$[1:5:2]'); - expect(slice.toString(), r'$[1:5:2]'); - expect(slice.read(abc).length, 2); - expect(slice.read(abc).first.value, 'b'); - expect(slice.read(abc).first.path, r'$[1]'); - expect(slice.read(abc).last.value, 'd'); - expect(slice.read(abc).last.path, r'$[3]'); - }); - test('1:5:-2', () { - final slice = JsonPath(r'$[1:5:-2]'); - expect(slice.toString(), r'$[1:5:-2]'); - expect(slice.read(abc).length, 0); - }); - test(':3', () { - final slice = JsonPath(r'$[:3]'); - expect(slice.toString(), r'$[:3]'); - expect(slice.read(abc).length, 3); - expect(slice.read(abc).first.value, 'a'); - expect(slice.read(abc).first.path, r'$[0]'); - expect(slice.read(abc).last.value, 'c'); - expect(slice.read(abc).last.path, r'$[2]'); - }); - test(':3:2', () { - final slice = JsonPath(r'$[:3:2]'); - expect(slice.toString(), r'$[:3:2]'); - expect(slice.read(abc).length, 2); - expect(slice.read(abc).first.value, 'a'); - expect(slice.read(abc).first.path, r'$[0]'); - expect(slice.read(abc).last.value, 'c'); - expect(slice.read(abc).last.path, r'$[2]'); - }); - test('3::2', () { - final slice = JsonPath(r'$[3::2]'); - expect(slice.toString(), r'$[3::2]'); - expect(slice.read(abc).length, 2); - expect(slice.read(abc).first.value, 'd'); - expect(slice.read(abc).first.path, r'$[3]'); - expect(slice.read(abc).last.value, 'f'); - expect(slice.read(abc).last.path, r'$[5]'); - }); - test('100:', () { - final slice = JsonPath(r'$[100:]'); - expect(slice.toString(), r'$[100:]'); - expect(slice.read(abc).length, 0); - }); - test('3:', () { - final slice = JsonPath(r'$[3:]'); - expect(slice.toString(), r'$[3:]'); - expect(slice.read(abc).length, 4); - expect(slice.read(abc).first.value, 'd'); - expect(slice.read(abc).first.path, r'$[3]'); - expect(slice.read(abc).last.value, 'g'); - expect(slice.read(abc).last.path, r'$[6]'); - }); - test(':-5', () { - final slice = JsonPath(r'$[:-5]'); - expect(slice.toString(), r'$[:-5]'); - expect(slice.read(abc).length, 2); - expect(slice.read(abc).first.value, 'a'); - expect(slice.read(abc).first.path, r'$[0]'); - expect(slice.read(abc).last.value, 'b'); - expect(slice.read(abc).last.path, r'$[1]'); - }); - - test('-5:', () { - final slice = JsonPath(r'$[-5:]'); - expect(slice.toString(), r'$[-5:]'); - expect(slice.read(abc).length, 5); - expect(slice.read(abc).first.value, 'c'); - expect(slice.read(abc).first.path, r'$[2]'); - expect(slice.read(abc).last.value, 'g'); - expect(slice.read(abc).last.path, r'$[6]'); - }); - test('0:6', () { - final slice = JsonPath(r'$[0:6]'); - expect(slice.toString(), r'$[:6]'); - expect(slice.read(abc).length, 6); - expect(slice.read(abc).first.value, 'a'); - expect(slice.read(abc).first.path, r'$[0]'); - expect(slice.read(abc).last.value, 'f'); - expect(slice.read(abc).last.path, r'$[5]'); - }); - test('0:100', () { - final slice = JsonPath(r'$[0:100]'); - expect(slice.toString(), r'$[:100]'); - expect(slice.read(abc).length, 7); - expect(slice.read(abc).first.value, 'a'); - expect(slice.read(abc).first.path, r'$[0]'); - expect(slice.read(abc).last.value, 'g'); - expect(slice.read(abc).last.path, r'$[6]'); - }); - - test('-6:-1', () { - final slice = JsonPath(r'$[-6:-1]'); - expect(slice.toString(), r'$[-6:-1]'); - expect(slice.read(abc).length, 5); - expect(slice.read(abc).first.value, 'b'); - expect(slice.read(abc).first.path, r'$[1]'); - expect(slice.read(abc).last.value, 'f'); - expect(slice.read(abc).last.path, r'$[5]'); - }); - }); - - group('Uncommon brackets', () { - test('Escape single quote', () { - final j = {r"sq'sq s\s qs\'qs": 'value'}; - final path = JsonPath(r"$['sq\'sq s\\s qs\\\'qs']"); - expect(path.toString(), r"$['sq\'sq s\\s qs\\\'qs']"); - final select = path.read(j); - expect(select.single.value, 'value'); - expect(select.single.path, r"$['sq\'sq s\\s qs\\\'qs']"); - }); - }); - - group('Union', () { - test('List', () { - final abc = 'abcdefg'.split(''); - final union = JsonPath(r'$[2,3,100,5]'); - expect(union.toString(), r'$[2,3,5,100]'); - expect(union.read(abc).length, 3); - expect(union.read(abc).first.value, 'c'); - expect(union.read(abc).first.path, r'$[2]'); - expect(union.read(abc).last.value, 'f'); - expect(union.read(abc).last.path, r'$[5]'); - }); - test('List with extra commas', () { - final union = JsonPath(r'$[,2,3, 100,,5, ]'); - expect(union.toString(), r'$[2,3,5,100]'); - }); - test('Object', () { - final abc = { - 'a': 'A', - 'b': 'B', - 'c': 'C', - }; - final union = JsonPath(r"$['a','x',c]"); - expect(union.toString(), r"$['a','x','c']"); - expect(union.read(abc).length, 2); - expect(union.read(abc).first.value, 'A'); - expect(union.read(abc).first.path, r"$['a']"); - expect(union.read(abc).last.value, 'C'); - expect(union.read(abc).last.path, r"$['c']"); - }); - test('Object with extra commas', () { - final union = JsonPath(r"$[,'a',,, 'x',c ,]"); - expect(union.toString(), r"$['a','x','c']"); - }); - test('Mixed union is object union', () { - final union = JsonPath(r"$[,5,'a',6]"); - expect(union.toString(), r"$['5','a','6']"); - }); - }); - - group('Wildcards', () { - test('All in root', () { - final allInRoot = JsonPath(r'$.*'); - expect(allInRoot.toString(), r'$.*'); - expect(allInRoot.read(json).length, 1); - expect(allInRoot.read(json).single.value, json['store']); - expect(allInRoot.read(json).single.path, r"$['store']"); - }); - - test('All in store', () { - final allInStore = JsonPath(r'$.store.*'); - expect(allInStore.toString(), r"$['store'].*"); - expect(allInStore.read(json).length, 2); - expect(allInStore.read(json).first.value, json['store']['book']); - expect(allInStore.read(json).first.path, r"$['store']['book']"); - expect(allInStore.read(json).last.value, json['store']['bicycle']); - expect(allInStore.read(json).last.path, r"$['store']['bicycle']"); - }); - test('No effect on scalars', () { - final allInStore = JsonPath(r'$.store.bicycle.color.*'); - expect(allInStore.toString(), r"$['store']['bicycle']['color'].*"); - expect(allInStore.read(json), isEmpty); - }); - }); - - group('Recursion', () { - test('Recursive', () { - final allNode = JsonPath(r'$..'); - expect(allNode.toString(), r'$..'); - expect(allNode.read(json).length, 8); - expect(allNode.read(json).first.value, json); - expect(allNode.read(json).first.path, r'$'); - expect(allNode.read(json).last.value, json['store']['bicycle']); - expect(allNode.read(json).last.path, r"$['store']['bicycle']"); - }); - - test('Recursive with all values', () { - final path = JsonPath(r'$..*'); - expect(path.toString(), r'$..*'); - expect(path.read(json).length, 27); - expect(path.read(json).first.value, json['store']); - expect(path.read(json).first.path, r"$['store']"); - expect(path.read(json).last.value, json['store']['bicycle']['price']); - expect(path.read(json).last.path, r"$['store']['bicycle']['price']"); - }); - - test('Every price tag', () { - final path = JsonPath(r'$..price'); - expect(path.toString(), r"$..['price']"); - expect(path.read(json).length, 5); - expect(path.read(json).first.value, json['store']['book'][0]['price']); - expect(path.read(json).first.path, r"$['store']['book'][0]['price']"); - expect(path.read(json).last.value, json['store']['bicycle']['price']); - expect(path.read(json).last.path, r"$['store']['bicycle']['price']"); - }); - }); - - group('Lists', () { - test('Path with an index', () { - final path = JsonPath(r'$.store.book[0].title'); - expect(path.toString(), r"$['store']['book'][0]['title']"); - expect(path.read(json).single.value, 'Sayings of the Century'); - expect(path.read(json).single.path, r"$['store']['book'][0]['title']"); - }); - - test('Last element of array (regression #1)', () { - final path = JsonPath(r"$['store']['book'][3]['price']"); - expect(path.toString(), r"$['store']['book'][3]['price']"); - expect(path.read(json).single.value, 22.99); - expect(path.read(json).single.path, r"$['store']['book'][3]['price']"); - }); - - test('All in list', () { - final path = JsonPath(r'$.store.book[*]'); - expect(path.toString(), r"$['store']['book'][*]"); - expect(path.read(json).length, 4); - expect(path.read(json).first.value, json['store']['book'][0]); - expect(path.read(json).first.path, r"$['store']['book'][0]"); - expect(path.read(json).last.value, json['store']['book'][3]); - expect(path.read(json).last.path, r"$['store']['book'][3]"); - }); - }); - - group('Filtering', () { - test('Simple', () { - final path = JsonPath(r'$.store..[?discounted]', filter: { - 'discounted': (e) => e is Map && e['price'] is num && e['price'] < 20 - }); - expect(path.toString(), r"$['store']..[?discounted]"); - expect(path.read(json).length, 4); - expect(path.read(json).first.value, json['store']['book'][0]); - expect(path.read(json).first.path, r"$['store']['book'][0]"); - expect(path.read(json).last.value, json['store']['bicycle']); - expect(path.read(json).last.path, r"$['store']['bicycle']"); - }); - - test('Can be applied to scalars', () { - final path = JsonPath(r'$.store..price[?low]', - filter: {'low': (e) => e is num && e < 20}); - expect(path.toString(), r"$['store']..['price'][?low]"); - expect(path.read(json).length, 4); - expect(path.read(json).first.value, json['store']['book'][0]['price']); - expect(path.read(json).first.path, r"$['store']['book'][0]['price']"); - expect(path.read(json).last.value, json['store']['bicycle']['price']); - expect(path.read(json).last.path, r"$['store']['bicycle']['price']"); - }); - - test('Missing filter', () { - expect(() => JsonPath(r'$.store..[?discounted]'), throwsFormatException); - }); - }); - test('Modifying in-place', () { - final someBooks = JsonPath(r'$.store.book[::2]'); - someBooks.read(json).forEach((result) { - result.value['title'] = 'Banana'; - }); - expect(json['store']['book'][0]['title'], 'Banana'); - expect(json['store']['book'][1]['title'], 'Sword of Honour'); - expect(json['store']['book'][2]['title'], 'Banana'); - expect(json['store']['book'][3]['title'], 'The Lord of the Rings'); - }); -} diff --git a/test/filters_test.dart b/test/filters_test.dart new file mode 100644 index 0000000..95b9f69 --- /dev/null +++ b/test/filters_test.dart @@ -0,0 +1,51 @@ +import 'dart:convert'; +import 'dart:io'; + +import 'package:json_path/json_path.dart'; +import 'package:test/test.dart'; + +void main() { + final json = jsonDecode(File('test/store.json').readAsStringSync()); + group('Filtering', () { + test('Simple', () { + final path = JsonPath(r'$.store..[?discounted]', filters: { + 'discounted': (m) => + m.value is Map && m.value['price'] is num && m.value['price'] < 20 + }); + + expect(path.toString(), r'$.store..[?discounted]'); + + final matches = path.read(json); + expect(matches.length, 4); + expect(matches.first.value, json['store']['book'][0]); + expect(matches.first.path, r"$['store']['book'][0]"); + expect(matches.last.value, json['store']['bicycle']); + expect(matches.last.path, r"$['store']['bicycle']"); + }); + + test('Can be applied to scalars, can access parent', () { + final path = JsonPath(r'$.store..price[?low]', filters: { + 'low': (m) => + m.value is num && + m.value < 20 && + m.parent?.value['category'] == 'fiction' + }); + + expect(path.toString(), r'$.store..price[?low]'); + + final matches = path.read(json); + expect(matches.length, 2); + expect(matches.first.value, json['store']['book'][1]['price']); + expect(matches.first.path, r"$['store']['book'][1]['price']"); + expect(matches.last.value, json['store']['book'][2]['price']); + expect(matches.last.path, r"$['store']['book'][2]['price']"); + }); + + test('Missing filter', () { + expect( + () => JsonPath(r'$.store..[?discounted]').read(json), + throwsA(predicate((e) => + e is FilterNotFound && e.toString().contains('discounted')))); + }); + }); +} diff --git a/test/parser_test.dart b/test/parser_test.dart new file mode 100644 index 0000000..0ca7f4a --- /dev/null +++ b/test/parser_test.dart @@ -0,0 +1,71 @@ +import 'package:json_path/src/build_parser.dart'; +import 'package:test/test.dart'; + +void main() { + group('Parser', () { + group('Valid expressions', () { + [ + r'$.foo.bar', + r'$.foo.*', + r'$[0]', + r'$[:]', + r'$[ ::]', + r'$[ 1:]', + r'$[1 : :]', + r'$[:42]', + r'$[2 :42 :]', + r'$[:2 :42]', + r'$[ : :3]', + r'$[1: -5: -2]', + r'$["foo 12 [] ? *bar привет"]', + r"$['foo 12 [] ? *bar привет']", + r'$[ 0, 2, -1 ]', + r'$.foo[*, -4, 3,:4, :, "foo" ]', + r'$..[0]', + r'$..*', + r'$.store.book[*].author', + r'$..author', + r'$.store.*', + r'$.store..price', + r'$..book[2]', + r'$..book[-1]', + r'$..book[0,1]', + r'$..book[:2]', + r'$.☺', + ].forEach((expr) { + test(expr, () { + final parser = buildParser().parse(expr); + if (parser.isFailure) { + fail(parser.message); + } + }); + }); + }); + group('Invalid expressions', () { + [ + r'', + r'$$', + r'.foo', + r'$....', + r'$...foo', + r'$[1+2]', + r'$[1874509822062987436598726432519879857164397163046130756769274369]', + r'$["""]', + r'$["]', + r'$[1 1]', + r'$[]', + r'$["\"]', + r'$["\z"]', + r'$[:::]', + r'$["foo"bar"]', + r"$['foo'bar']", + ].forEach((expr) { + test(expr, () { + try { + expect(buildParser().parse(expr).isFailure, isTrue); + } on FormatException catch (_) {} + }); + }); + }); + }); +} diff --git a/test/parsing_test.dart b/test/parsing_test.dart deleted file mode 100644 index 05bfc40..0000000 --- a/test/parsing_test.dart +++ /dev/null @@ -1,9 +0,0 @@ -import 'package:json_path/json_path.dart'; -import 'package:test/test.dart'; - -void main() { - test('FormatException is thrown', () { - expect(() => JsonPath(r'$.foo[?]'), throwsFormatException); - expect(() => JsonPath(r'$.foo]'), throwsFormatException); - }); -} diff --git a/test/set_test.dart b/test/set_test.dart deleted file mode 100644 index e22a1ec..0000000 --- a/test/set_test.dart +++ /dev/null @@ -1,153 +0,0 @@ -import 'dart:convert'; -import 'dart:io'; - -import 'package:json_path/json_path.dart'; -import 'package:test/test.dart'; - -void main() { - final json = jsonDecode(File('test/store.json').readAsStringSync()); - test('Root element', () { - final root = JsonPath(r'$'); - expect(root.set(json, 'foo'), 'foo'); - }); - group('Field', () { - final store = JsonPath(r'$.store'); - test('Replaces existing field', () { - expect(store.set(json, 'foo'), {'store': 'foo'}); - }); - test('Can be nested', () { - final color = JsonPath(r'$.store.bicycle.color'); - final mutated = color.set(json, 'blue') as dynamic; - expect(mutated['store']['bicycle']['color'], 'blue'); - expect(mutated['store']['bicycle']['price'], - json['store']['bicycle']['price']); - expect(mutated['store']['book'], json['store']['book']); - expect(json['store']['bicycle']['color'], 'red', - reason: 'Original bike is still red'); - }); - test('Creates a field if it does not exist', () { - final abc = JsonPath(r'$.a.b.c'); - final mutated = abc.set({'foo': 'bar'}, 'magic'); - expect(mutated['foo'], 'bar'); - expect(mutated['a']['b']['c'], 'magic'); - }); - test('Does not change non-object (scalars, arrays)', () { - expect(store.set(42, 'foo'), 42); - expect(store.set([true], 'foo'), [true]); - }); - test('Replace all bicycle fields with a banana', () { - final bikeFields = JsonPath(r'$.store.bicycle.*'); - final mutated = bikeFields.set(json, 'banana'); - expect(mutated['store']['bicycle']['color'], 'banana'); - expect(mutated['store']['bicycle']['price'], 'banana'); - }); - }); - - test('Recursive. Set all prices to 0', () { - final prices = JsonPath(r'$..price'); - final mutated = prices.set(json, 0); - expect(mutated['store']['bicycle']['price'], 0); - expect(mutated['store']['book'][0]['price'], 0); - expect(mutated['store']['book'][1]['price'], 0); - expect(mutated['store']['book'][2]['price'], 0); - expect(mutated['store']['book'][3]['price'], 0); - }); - - test('Filter. Hide prices which are too high', () { - final prices = JsonPath(r'$..price[?high]', - filter: {'high': (_) => _ is num && _ > 15}); - final mutated = prices.set(json, 'hidden'); - expect(mutated['store']['bicycle']['price'], 'hidden'); - expect(mutated['store']['book'][0]['price'], 8.95); - expect(mutated['store']['book'][3]['price'], 'hidden'); - }); - - group('Index', () { - test('First element', () { - final price = JsonPath(r'$.store.book[0].price'); - final mutated = price.set(json, 'hidden'); - expect(mutated['store']['bicycle']['price'], isA()); - expect(mutated['store']['book'][0]['price'], 'hidden'); - expect(mutated['store']['book'][1]['price'], isA()); - expect(mutated['store']['book'][2]['price'], isA()); - expect(mutated['store']['book'][3]['price'], isA()); - }); - test('Union', () { - final price = JsonPath(r'$.store.book[1,3].price'); - final mutated = price.set(json, 'hidden'); - expect(mutated['store']['bicycle']['price'], isA()); - expect(mutated['store']['book'][0]['price'], isA()); - expect(mutated['store']['book'][1]['price'], 'hidden'); - expect(mutated['store']['book'][2]['price'], isA()); - expect(mutated['store']['book'][3]['price'], 'hidden'); - }); - test('Wildcard', () { - final price = JsonPath(r'$.store.book[*].price'); - final mutated = price.set(json, 'hidden'); - expect(mutated['store']['book'][0]['price'], 'hidden'); - expect(mutated['store']['book'][1]['price'], 'hidden'); - expect(mutated['store']['book'][2]['price'], 'hidden'); - expect(mutated['store']['book'][3]['price'], 'hidden'); - }); - test('Slice', () { - final price = JsonPath(r'$.store.book[::2]'); - final mutated = price.set(json, 'banana'); - expect(mutated['store']['book'][0], 'banana'); - expect(mutated['store']['book'][1]['price'], isA()); - expect(mutated['store']['book'][2], 'banana'); - expect(mutated['store']['book'][3]['price'], isA()); - }); - test('Create list with a single index', () { - final ab0c = JsonPath(r'$.a.b[0].c'); - expect(ab0c.set({}, 'Banana'), { - 'a': { - 'b': [ - {'c': 'Banana'} - ] - } - }); - }); - test('Create list with a single index (-0)', () { - final ab0c = JsonPath(r'$.a.b[-0].c'); - expect(ab0c.set({}, 'Banana'), { - 'a': { - 'b': [ - {'c': 'Banana'} - ] - } - }); - }); - test('Create list with a multiple adjacent indices', () { - final ab0c = JsonPath(r'$.a.b[0,2, 1,2,1,].c'); - expect(ab0c.set({}, 'Banana'), { - 'a': { - 'b': [ - {'c': 'Banana'}, - {'c': 'Banana'}, - {'c': 'Banana'}, - ] - } - }); - }); - test('Setting non-existing adjacent index creates new element', () { - final title = JsonPath(r'$.store.book[5,4].title'); - final mutated = title.set(json, 'My Book'); - expect(json['store']['book'].length, 4); - expect(mutated['store']['book'].length, 6); - expect(mutated['store']['book'][4]['title'], 'My Book'); - expect(mutated['store']['book'][5]['title'], 'My Book'); - }); - test('A gap in the indices throws RangeError', () { - final ab0c = JsonPath(r'$.a.b[3,1].c'); - expect(() => ab0c.set({}, 'Banana'), throwsRangeError); - }); - test('Setting non-existing non-adjacent index throws RangeError', () { - final title = JsonPath(r'$.store.book[100].title'); - expect(() => title.set(json, 'Banana'), throwsRangeError); - }); - test('Setting negative index throws RangeError', () { - final title = JsonPath(r'$.store.book[-1].title'); - expect(() => title.set(json, 'Banana'), throwsRangeError); - }); - }); -}