Skip to content

Commit

Permalink
Add recursive selector
Browse files Browse the repository at this point in the history
  • Loading branch information
f3ath committed Jul 28, 2020
1 parent 4924dde commit b9e7eef
Show file tree
Hide file tree
Showing 7 changed files with 92 additions and 38 deletions.
2 changes: 1 addition & 1 deletion lib/src/json_path.dart
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ class JsonPath {
for (final node in Node.build(tokenize(expression)).children) {
state = state.process(node);
}
return JsonPath._(state.filter);
return JsonPath._(state.selector);
}

JsonPath._(this._selector);
Expand Down
12 changes: 12 additions & 0 deletions lib/src/quote.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
/// A single-quoted string.
/// Example:
/// `hello` => `'hello'`
/// `i'm ok` => `'i\'m ok'`
class Quote {
Quote(String value) : value = "'${value.replaceAll("'", r"\'")}'";

final String value;

@override
String toString() => value;
}
5 changes: 3 additions & 2 deletions lib/src/selector/field.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import 'package:jessie/src/selector/selector.dart';
import 'package:jessie/src/quote.dart';
import 'package:jessie/src/result.dart';
import 'package:jessie/src/selector/selector.dart';

class Field extends Selector {
Field(this.name);
Expand All @@ -12,5 +13,5 @@ class Field extends Selector {
.map((r) => Result(r.value[name], r.path + toString()));

@override
String get expression => "['$name']";
String get expression => '[${Quote(name)}]';
}
25 changes: 25 additions & 0 deletions lib/src/selector/recursive.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import 'package:jessie/src/quote.dart';
import 'package:jessie/src/result.dart';
import 'package:jessie/src/selector/selector.dart';

class Recursive extends Selector {
@override
Iterable<Result> call(Iterable<Result> results) => results.map((r) {
final val = r.value;
final self = [r];
if (val is Map) {
return self.followedBy(call(val.entries
.map((e) => Result(e.value, r.path + '[${Quote(e.key)}]'))));
}
if (val is List) {
return self.followedBy(call(val
.asMap()
.entries
.map((e) => Result(e.value, r.path + '[${e.key}]'))));
}
return <Result>[];
}).expand((_) => _);

@override
String get expression => '..';
}
25 changes: 14 additions & 11 deletions lib/src/state.dart
Original file line number Diff line number Diff line change
@@ -1,51 +1,54 @@
import 'package:jessie/src/ast.dart';
import 'package:jessie/src/selector/all_in_array.dart';
import 'package:jessie/src/selector/field.dart';
import 'package:jessie/src/selector/selector.dart';
import 'package:jessie/src/selector/index.dart';
import 'package:jessie/src/selector/recursive.dart';
import 'package:jessie/src/selector/selector.dart';

abstract class State {
State process(Node node);

Selector get filter;
Selector get selector;
}

class Ready implements State {
Ready(this.filter);
Ready(this.selector);

@override
final Selector filter;
final Selector selector;

@override
State process(Node node) {
if (node.value == '[') {
return Ready(filter.then(_brackets(node.children)));
return Ready(selector.then(_brackets(node.children)));
}
if (node.value == '.') {
return AwaitingField(filter);
return AwaitingField(selector);
}
if (node.value == '..') {
return Ready(selector.then(Recursive()));
}
throw StateError('Got ${node.value} in $this');
}

Selector _brackets(List<Node> nodes) {
if (nodes.length == 1) {
final node = nodes.single;
final val = node.value;
if (val == '*') return AllInArray();
if (node.value == '*') return AllInArray();
if (node.isNumber) return Index(int.parse(nodes.first.value));
}
throw StateError('Unexpected bracket expression');
}
}

class AwaitingField implements State {
AwaitingField(this.filter);
AwaitingField(this.selector);

@override
final Selector filter;
final Selector selector;

@override
State process(Node node) {
return Ready(filter.then(Field(node.value)));
return Ready(selector.then(Field(node.value)));
}
}
2 changes: 1 addition & 1 deletion lib/src/tokenize.dart
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ List<String> tokenize(String expr) {
break;
}
if (expr.length > pos + 1) {
final nextTwoChars = expr.substring(pos, pos + 1);
final nextTwoChars = expr[pos] + expr[pos + 1];
if (_doubles.contains(nextTwoChars)) {
token = nextTwoChars;
pos += 2;
Expand Down
59 changes: 36 additions & 23 deletions test/json_path_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,41 +8,54 @@ void main() {
final json = jsonDecode(File('test/store.json').readAsStringSync());
group('Basic expressions', () {
test('Empty', () {
final path = JsonPath('');
expect(path.toString(), r'$');
expect(path.select(json).single.value, json);
expect(path.select(json).single.path, r'$');
final empty = JsonPath('');
expect(empty.toString(), r'$');
expect(empty.select(json).single.value, json);
expect(empty.select(json).single.path, r'$');
});

test('Only root', () {
final path = JsonPath(r'$');
expect(path.toString(), r'$');
expect(path.select(json).single.value, json);
expect(path.select(json).single.path, r'$');
final root = JsonPath(r'$');
expect(root.toString(), r'$');
expect(root.select(json).single.value, json);
expect(root.select(json).single.path, r'$');
});

test('Single field', () {
final path = JsonPath(r'$.store');
expect(path.toString(), r"$['store']");
expect(path.select(json).single.value, json['store']);
expect(path.select(json).single.path, r"$['store']");
final store = JsonPath(r'$.store');
expect(store.toString(), r"$['store']");
expect(store.select(json).single.value, json['store']);
expect(store.select(json).single.path, r"$['store']");
});

test('Path with an index', () {
final path = JsonPath(r'$.store.book[0].title');
expect(path.toString(), r"$['store']['book'][0]['title']");
expect(path.select(json).single.value, 'Sayings of the Century');
expect(path.select(json).single.path, r"$['store']['book'][0]['title']");
final firstBookTitle = JsonPath(r'$.store.book[0].title');
expect(firstBookTitle.toString(), r"$['store']['book'][0]['title']");
expect(
firstBookTitle.select(json).single.value, 'Sayings of the Century');
expect(firstBookTitle.select(json).single.path,
r"$['store']['book'][0]['title']");
});

test('All in array', () {
final path = JsonPath(r'$.store.book[*]');
expect(path.toString(), r"$['store']['book'][*]");
expect(path.select(json).length, 4);
expect(path.select(json).first.value, json['store']['book'][0]);
expect(path.select(json).first.path, r"$['store']['book'][0]");
expect(path.select(json).last.value, json['store']['book'][3]);
expect(path.select(json).last.path, r"$['store']['book'][3]");
final allBooksInStore = JsonPath(r'$.store.book[*]');
expect(allBooksInStore.toString(), r"$['store']['book'][*]");
expect(allBooksInStore.select(json).length, 4);
expect(
allBooksInStore.select(json).first.value, json['store']['book'][0]);
expect(allBooksInStore.select(json).first.path, r"$['store']['book'][0]");
expect(allBooksInStore.select(json).last.value, json['store']['book'][3]);
expect(allBooksInStore.select(json).last.path, r"$['store']['book'][3]");
});

test('Recursive', () {
final everything = JsonPath(r'$..');
expect(everything.toString(), r'$..');
expect(everything.select(json).length, 8);
expect(everything.select(json).first.value, json);
expect(everything.select(json).first.path, r'$');
expect(everything.select(json).last.value, json['store']['bicycle']);
expect(everything.select(json).last.path, r"$['store']['bicycle']");
});
});
}

0 comments on commit b9e7eef

Please sign in to comment.