Skip to content

Commit

Permalink
Slice
Browse files Browse the repository at this point in the history
  • Loading branch information
f3ath committed Aug 1, 2020
1 parent c36a0f3 commit 2c40680
Show file tree
Hide file tree
Showing 6 changed files with 202 additions and 13 deletions.
1 change: 0 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)]`)
Expand Down
46 changes: 46 additions & 0 deletions lib/src/selector/slice.dart
Original file line number Diff line number Diff line change
@@ -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<Result> call(Iterable<Result> results) => results.map((r) {
if (step > 0 && r.value is List) {
return _filterList(r.value, r.path);
}
return const <Result>[];
}).expand((_) => _);

@override
String get expression =>
'[${first == 0 ? '' : first}:${last ?? ''}${step != 1 ? ':$step' : ''}]';

Iterable<Result> _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<int> _for(int from, int to, int step) sync* {
for (var i = from; i < to; i += step) {
yield i;
}
}
}
41 changes: 33 additions & 8 deletions lib/src/state.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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 '..':
Expand All @@ -38,15 +39,39 @@ class Ready implements State {
}
}

Selector _bracketExpression(List<Node> 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<Node> 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 {
Expand Down
2 changes: 2 additions & 0 deletions lib/src/tokenize.dart
Original file line number Diff line number Diff line change
Expand Up @@ -46,12 +46,14 @@ List<String> tokenize(String expr) {
return tokens;
}


const _singles = [
r'$',
'[',
']',
'.',
'*',
':',
];

const _doubles = [
Expand Down
1 change: 1 addition & 0 deletions pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
124 changes: 120 additions & 4 deletions test/json_path_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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']");
});
});

Expand All @@ -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'};
Expand Down

0 comments on commit 2c40680

Please sign in to comment.