From 2c406805854302bf915d6bfd2c09586c4310e3eb Mon Sep 17 00:00:00 2001 From: f3ath Date: Fri, 31 Jul 2020 22:07:37 -0700 Subject: [PATCH] Slice --- README.md | 1 - lib/src/selector/slice.dart | 46 +++++++++++++ lib/src/state.dart | 41 +++++++++--- lib/src/tokenize.dart | 2 + pubspec.yaml | 1 + test/json_path_test.dart | 124 ++++++++++++++++++++++++++++++++++-- 6 files changed, 202 insertions(+), 13 deletions(-) create mode 100644 lib/src/selector/slice.dart diff --git a/README.md b/README.md index 44430f9..ef77093 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,6 @@ - [x] Recursive descent (`$..`) - [x] Wildcard (`$.store.*`) - [x] Square-bracket field notation (`['foo']`, `$['"some" \'special\' [chars]']`) -- [ ] Subscript (`books[:2]`) - [ ] Slice (`articles[1:10:2]`) - [ ] Union (`book[0, 1]`, `book[author, title, price]`) - [ ] Basic filtering (`book[?(@.price - 1)]`) diff --git a/lib/src/selector/slice.dart b/lib/src/selector/slice.dart new file mode 100644 index 0000000..2b7ac2c --- /dev/null +++ b/lib/src/selector/slice.dart @@ -0,0 +1,46 @@ +import 'dart:math'; + +import 'package:json_path/src/result.dart'; +import 'package:json_path/src/selector/selector.dart'; + +class Slice extends Selector { + Slice({int first, this.last, int step}) + : first = first ?? 0, + step = step ?? 1; + + final int first; + + final int last; + + final int step; + + @override + Iterable call(Iterable results) => results.map((r) { + if (step > 0 && r.value is List) { + return _filterList(r.value, r.path); + } + return const []; + }).expand((_) => _); + + @override + String get expression => + '[${first == 0 ? '' : first}:${last ?? ''}${step != 1 ? ':$step' : ''}]'; + + Iterable _filterList(List list, String path) => + _for(_actualFirst(list.length), _actualLast(list.length), step) + .map((i) => Result(list[i], path + '[$i]')); + + 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); + } + + Iterable _for(int from, int to, int step) sync* { + for (var i = from; i < to; i += step) { + yield i; + } + } +} diff --git a/lib/src/state.dart b/lib/src/state.dart index fbafb38..d5e47de 100644 --- a/lib/src/state.dart +++ b/lib/src/state.dart @@ -5,6 +5,7 @@ import 'package:json_path/src/selector/field.dart'; import 'package:json_path/src/selector/index.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 State { @@ -26,7 +27,7 @@ class Ready implements State { State process(Node node) { switch (node.value) { case '[': - return Ready(selector.then(_bracketExpression(node.children))); + return Ready(selector.then(_bracketSelector(node.children))); case '.': return AwaitingField(selector); case '..': @@ -38,15 +39,39 @@ class Ready implements State { } } - Selector _bracketExpression(List nodes) { - final node = nodes.single; - if (node.value == '*') return AllInArray(); - if (node.isNumber) return Index(int.parse(nodes.first.value)); - if (node.value.startsWith("'")) return Field(_unquote(node.value)); + Selector _bracketSelector(List nodes) { + if (nodes.length == 1) { + final node = nodes.single; + if (node.value == '*') return AllInArray(); + if (node.isNumber) return Index(int.parse(nodes.first.value)); + if (node.value.startsWith("'")) { + return Field(node.value.substring(1, node.value.length - 1)); + } + } else if (nodes.length > 1) { + int first; + int last; + int step; + var colons = 0; + nodes.map((_) => _.value).forEach((val) { + if (val == ':') { + colons++; + return; + } + if (colons == 0) { + first = int.parse(val); + return; + } + if (colons == 1) { + last = int.parse(val); + return; + } + step = int.parse(val); + }); + return Slice(first: first, last: last, step: step); + } + throw StateError('Unexpected bracket expression'); } - - String _unquote(String s) => s.substring(1, s.length - 1); } class AwaitingField implements State { diff --git a/lib/src/tokenize.dart b/lib/src/tokenize.dart index a6967c9..b721e82 100644 --- a/lib/src/tokenize.dart +++ b/lib/src/tokenize.dart @@ -46,12 +46,14 @@ List tokenize(String expr) { return tokens; } + const _singles = [ r'$', '[', ']', '.', '*', + ':', ]; const _doubles = [ diff --git a/pubspec.yaml b/pubspec.yaml index 5361da2..7f1ba38 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -3,6 +3,7 @@ version: 0.0.0+dev.4 description: JSONPath for Dart. JSONPath is XPath for JSON. It is a path in a JSON document. homepage: "https://github.com/f3ath/jessie" + dev_dependencies: pedantic: ^1.9.0 test: ^1.9.0 diff --git a/test/json_path_test.dart b/test/json_path_test.dart index 9a477bf..7d516d9 100644 --- a/test/json_path_test.dart +++ b/test/json_path_test.dart @@ -36,11 +36,11 @@ void main() { }); test('Mixed brackets and fields', () { - final store = JsonPath(r"$['store'].bicycle['price']"); - expect(store.toString(), r"$['store']['bicycle']['price']"); + final price = JsonPath(r"$['store'].bicycle['price']"); + expect(price.toString(), r"$['store']['bicycle']['price']"); expect( - store.select(json).single.value, json['store']['bicycle']['price']); - expect(store.select(json).single.path, r"$['store']['bicycle']['price']"); + price.select(json).single.value, json['store']['bicycle']['price']); + expect(price.select(json).single.path, r"$['store']['bicycle']['price']"); }); }); @@ -51,6 +51,122 @@ void main() { }); }); + group('Slices', () { + final abc = 'abcdefg'.split(''); + test('1:3', () { + final slice = JsonPath(r'$[1:3]'); + expect(slice.toString(), r'$[1:3]'); + expect(slice.select(abc).length, 2); + expect(slice.select(abc).first.value, 'b'); + expect(slice.select(abc).first.path, r'$[1]'); + expect(slice.select(abc).last.value, 'c'); + expect(slice.select(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.select(abc).length, 2); + expect(slice.select(abc).first.value, 'b'); + expect(slice.select(abc).first.path, r'$[1]'); + expect(slice.select(abc).last.value, 'd'); + expect(slice.select(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.select(abc).length, 0); + }); + test(':3', () { + final slice = JsonPath(r'$[:3]'); + expect(slice.toString(), r'$[:3]'); + expect(slice.select(abc).length, 3); + expect(slice.select(abc).first.value, 'a'); + expect(slice.select(abc).first.path, r'$[0]'); + expect(slice.select(abc).last.value, 'c'); + expect(slice.select(abc).last.path, r'$[2]'); + }); + test(':3:2', () { + final slice = JsonPath(r'$[:3:2]'); + expect(slice.toString(), r'$[:3:2]'); + expect(slice.select(abc).length, 2); + expect(slice.select(abc).first.value, 'a'); + expect(slice.select(abc).first.path, r'$[0]'); + expect(slice.select(abc).last.value, 'c'); + expect(slice.select(abc).last.path, r'$[2]'); + }); + test('3::2', () { + final slice = JsonPath(r'$[3::2]'); + expect(slice.toString(), r'$[3::2]'); + expect(slice.select(abc).length, 2); + expect(slice.select(abc).first.value, 'd'); + expect(slice.select(abc).first.path, r'$[3]'); + expect(slice.select(abc).last.value, 'f'); + expect(slice.select(abc).last.path, r'$[5]'); + }); + test('100:', () { + final slice = JsonPath(r'$[100:]'); + expect(slice.toString(), r'$[100:]'); + expect(slice.select(abc).length, 0); + }); + test('3:', () { + final slice = JsonPath(r'$[3:]'); + expect(slice.toString(), r'$[3:]'); + expect(slice.select(abc).length, 4); + expect(slice.select(abc).first.value, 'd'); + expect(slice.select(abc).first.path, r'$[3]'); + expect(slice.select(abc).last.value, 'g'); + expect(slice.select(abc).last.path, r'$[6]'); + }); + test(':-5', () { + final slice = JsonPath(r'$[:-5]'); + expect(slice.toString(), r'$[:-5]'); + expect(slice.select(abc).length, 2); + expect(slice.select(abc).first.value, 'a'); + expect(slice.select(abc).first.path, r'$[0]'); + expect(slice.select(abc).last.value, 'b'); + expect(slice.select(abc).last.path, r'$[1]'); + }); + + test('-5:', () { + final slice = JsonPath(r'$[-5:]'); + expect(slice.toString(), r'$[-5:]'); + expect(slice.select(abc).length, 5); + expect(slice.select(abc).first.value, 'c'); + expect(slice.select(abc).first.path, r'$[2]'); + expect(slice.select(abc).last.value, 'g'); + expect(slice.select(abc).last.path, r'$[6]'); + }); + test('0:6', () { + final slice = JsonPath(r'$[0:6]'); + expect(slice.toString(), r'$[:6]'); + expect(slice.select(abc).length, 6); + expect(slice.select(abc).first.value, 'a'); + expect(slice.select(abc).first.path, r'$[0]'); + expect(slice.select(abc).last.value, 'f'); + expect(slice.select(abc).last.path, r'$[5]'); + }); + test('0:100', () { + final slice = JsonPath(r'$[0:100]'); + expect(slice.toString(), r'$[:100]'); + expect(slice.select(abc).length, 7); + expect(slice.select(abc).first.value, 'a'); + expect(slice.select(abc).first.path, r'$[0]'); + expect(slice.select(abc).last.value, 'g'); + expect(slice.select(abc).last.path, r'$[6]'); + }); + + test('-6:-1', () { + final slice = JsonPath(r'$[-6:-1]'); + expect(slice.toString(), r'$[-6:-1]'); + expect(slice.select(abc).length, 5); + expect(slice.select(abc).first.value, 'b'); + expect(slice.select(abc).first.path, r'$[1]'); + expect(slice.select(abc).last.value, 'f'); + expect(slice.select(abc).last.path, r'$[5]'); + }); + + }); + group('Uncommon brackets', () { test('Escape single quote', () { final j = {r"sq'sq s\s qs\'qs": 'value'};