diff --git a/CHANGELOG.md b/CHANGELOG.md index f27d01f..3e70a5b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,9 +1,12 @@ ## [Unreleased] +## [0.0.0+dev.1] - 2020-07-27 ### Added - Tokenizer and AST +- All-in-array selector ## 0.0.0+dev.0 - 2020-07-24 ### Added - Basic design draft -[Unreleased]: https://github.com/f3ath/jessie/compare/0.0.0+dev.0...HEAD \ No newline at end of file +[Unreleased]: https://github.com/f3ath/jessie/compare/0.0.0+dev.1...HEAD +[0.0.0+dev.1]: https://github.com/f3ath/jessie/compare/0.0.0+dev.0...0.0.0+dev.1 \ No newline at end of file diff --git a/README.md b/README.md index 5916b6b..7a373e8 100644 --- a/README.md +++ b/README.md @@ -2,9 +2,9 @@ Jessie is a work-in-progress. Expect the API to change often. Feel free to join. ## Roadmap -- [ ] Basic selectors: fields, indices +- [x] Basic selectors: fields, indices - [ ] Recursive descent (`..`) -- [ ] Wildcard (`*`) +- [x] Wildcard (`*`) - [ ] Subscript (`[:2]`) - [ ] Slice (`[1:10:2]`) - [ ] Union (`book[0, 1]`, `book[author, title, price]`) diff --git a/lib/jessie.dart b/lib/jessie.dart index ce0a1fd..3799019 100644 --- a/lib/jessie.dart +++ b/lib/jessie.dart @@ -2,3 +2,4 @@ library jessie; export 'package:jessie/src/json_path.dart'; +export 'package:jessie/src/result.dart'; diff --git a/lib/src/ast.dart b/lib/src/ast.dart index 74cb7d8..7663133 100644 --- a/lib/src/ast.dart +++ b/lib/src/ast.dart @@ -1,6 +1,7 @@ class Node { Node(this.value); + /// Builds the AST from the list of tokens static Node build(List tokens) { final root = Node(r'$'); if (tokens.isEmpty) { @@ -24,7 +25,7 @@ class Node { children.add(stack.removeLast()); } final brackets = stack.removeLast(); - brackets.children.addAll(children); + brackets.children.addAll(children.reversed); stack.last.children.add(brackets); continue; } @@ -35,4 +36,6 @@ class Node { final String value; final children = []; + + bool get isNumber => RegExp(r'^-?\d+$').hasMatch(value); } diff --git a/lib/src/field.dart b/lib/src/field.dart deleted file mode 100644 index 55428f0..0000000 --- a/lib/src/field.dart +++ /dev/null @@ -1,16 +0,0 @@ -import 'package:jessie/src/filter.dart'; -import 'package:jessie/src/match.dart'; - -class Field extends Filter { - Field(this.name); - - final String name; - - @override - Iterable call(Iterable matches) => matches - .where((m) => m.value is Map && m.value.containsKey(name)) - .map((m) => PathMatch(m.value[name], m.path + toString())); - - @override - String toString() => "['$name']"; -} diff --git a/lib/src/filter.dart b/lib/src/filter.dart deleted file mode 100644 index f4d8e03..0000000 --- a/lib/src/filter.dart +++ /dev/null @@ -1,33 +0,0 @@ -import 'package:jessie/src/match.dart'; - -abstract class Filter { - const Filter(); - - /// Applies this filter to the [matches] - Iterable call(Iterable matches); - - /// The string expression without leading `$` - @override - String toString(); - - /// A shortcut for `then()` - Filter operator |(Filter other) => then(other); - - /// Combines this expression with the [other] - Filter then(Filter other) => _Chain(this, other); -} - -class _Chain extends Filter { - _Chain(this.first, this.second); - - final Filter first; - - final Filter second; - - @override - Iterable call(Iterable matches) => - second(first(matches)); - - @override - String toString() => '$first$second'; -} diff --git a/lib/src/index.dart b/lib/src/index.dart deleted file mode 100644 index f1626c9..0000000 --- a/lib/src/index.dart +++ /dev/null @@ -1,16 +0,0 @@ -import 'package:jessie/src/filter.dart'; -import 'package:jessie/src/match.dart'; - -class Index extends Filter { - Index(this.index); - - final int index; - - @override - Iterable call(Iterable matches) => matches - .where((m) => m.value is List && m.value.length > index + 1) - .map((m) => PathMatch(m.value[index], m.path + toString())); - - @override - String toString() => '[$index]'; -} diff --git a/lib/src/json_path.dart b/lib/src/json_path.dart index c56932f..c23f8f3 100644 --- a/lib/src/json_path.dart +++ b/lib/src/json_path.dart @@ -1,7 +1,7 @@ import 'package:jessie/src/ast.dart'; -import 'package:jessie/src/filter.dart'; -import 'package:jessie/src/match.dart'; -import 'package:jessie/src/root.dart'; +import 'package:jessie/src/selector/selector.dart'; +import 'package:jessie/src/selector/root.dart'; +import 'package:jessie/src/result.dart'; import 'package:jessie/src/state.dart'; import 'package:jessie/src/tokenize.dart'; @@ -14,14 +14,14 @@ class JsonPath { return JsonPath._(state.filter); } - JsonPath._(this._filter); + JsonPath._(this._selector); - final Filter _filter; + final Selector _selector; /// Filters the given [json]. /// Returns an Iterable of all elements found - Iterable filter(json) => _filter.call([PathMatch(json, '')]); + Iterable select(json) => _selector([Result(json, '')]); @override - String toString() => _filter.toString(); + String toString() => _selector.toString(); } diff --git a/lib/src/match.dart b/lib/src/match.dart deleted file mode 100644 index 4182d65..0000000 --- a/lib/src/match.dart +++ /dev/null @@ -1,7 +0,0 @@ -class PathMatch { - PathMatch(this.value, this.path); - - final T value; - - final String path; -} diff --git a/lib/src/result.dart b/lib/src/result.dart new file mode 100644 index 0000000..488e23b --- /dev/null +++ b/lib/src/result.dart @@ -0,0 +1,10 @@ +/// A single matching result +class Result { + Result(this.value, this.path); + + /// The value + final T value; + + /// JSONPath to this result + final String path; +} diff --git a/lib/src/root.dart b/lib/src/root.dart deleted file mode 100644 index 3fdc028..0000000 --- a/lib/src/root.dart +++ /dev/null @@ -1,13 +0,0 @@ -import 'package:jessie/src/filter.dart'; -import 'package:jessie/src/match.dart'; - -class Root extends Filter { - const Root(); - - @override - Iterable call(Iterable matches) => - matches.map((m) => PathMatch(m.value, toString())); - - @override - String toString() => r'$'; -} diff --git a/lib/src/selector/all_in_array.dart b/lib/src/selector/all_in_array.dart new file mode 100644 index 0000000..e11ec78 --- /dev/null +++ b/lib/src/selector/all_in_array.dart @@ -0,0 +1,18 @@ +import 'package:jessie/src/selector/selector.dart'; +import 'package:jessie/src/result.dart'; + +class AllInArray extends Selector { + @override + Iterable call(Iterable results) => + results.where((r) => r.value is List).map((r) { + final val = r.value as List; + final results = []; + for (var i = 0; i < val.length; i++) { + results.add(Result(val[i], r.path + '[$i]')); + } + return results; + }).expand((_) => _); + + @override + String get expression => '[*]'; +} diff --git a/lib/src/selector/field.dart b/lib/src/selector/field.dart new file mode 100644 index 0000000..0e47f65 --- /dev/null +++ b/lib/src/selector/field.dart @@ -0,0 +1,16 @@ +import 'package:jessie/src/selector/selector.dart'; +import 'package:jessie/src/result.dart'; + +class Field extends Selector { + Field(this.name); + + final String name; + + @override + Iterable call(Iterable results) => results + .where((r) => r.value is Map && r.value.containsKey(name)) + .map((r) => Result(r.value[name], r.path + toString())); + + @override + String get expression => "['$name']"; +} diff --git a/lib/src/selector/index.dart b/lib/src/selector/index.dart new file mode 100644 index 0000000..1b62a73 --- /dev/null +++ b/lib/src/selector/index.dart @@ -0,0 +1,16 @@ +import 'package:jessie/src/selector/selector.dart'; +import 'package:jessie/src/result.dart'; + +class Index extends Selector { + Index(this.index); + + final int index; + + @override + Iterable call(Iterable results) => results + .where((r) => r.value is List && r.value.length > index + 1) + .map((r) => Result(r.value[index], r.path + toString())); + + @override + String get expression => '[$index]'; +} diff --git a/lib/src/selector/root.dart b/lib/src/selector/root.dart new file mode 100644 index 0000000..0c3b45a --- /dev/null +++ b/lib/src/selector/root.dart @@ -0,0 +1,13 @@ +import 'package:jessie/src/selector/selector.dart'; +import 'package:jessie/src/result.dart'; + +class Root extends Selector { + const Root(); + + @override + Iterable call(Iterable results) => + results.map((m) => Result(m.value, toString())); + + @override + String get expression => r'$'; +} diff --git a/lib/src/selector/selector.dart b/lib/src/selector/selector.dart new file mode 100644 index 0000000..04ab5f7 --- /dev/null +++ b/lib/src/selector/selector.dart @@ -0,0 +1,32 @@ +import 'package:jessie/src/result.dart'; + +/// Converts a set of results into a set of results +abstract class Selector { + const Selector(); + + /// Applies this filter to the [results] + Iterable call(Iterable results); + + /// The filter expression as string + String get expression; + + @override + String toString() => expression; + + /// Combines this expression with the [other] + Selector then(Selector other) => _Chain(this, other); +} + +class _Chain extends Selector { + _Chain(this.first, this.second); + + final Selector first; + + final Selector second; + + @override + Iterable call(Iterable results) => second(first(results)); + + @override + String get expression => '$first$second'; +} diff --git a/lib/src/state.dart b/lib/src/state.dart index 88b59d4..2276dbd 100644 --- a/lib/src/state.dart +++ b/lib/src/state.dart @@ -1,37 +1,48 @@ import 'package:jessie/src/ast.dart'; -import 'package:jessie/src/field.dart'; -import 'package:jessie/src/filter.dart'; -import 'package:jessie/src/index.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'; abstract class State { State process(Node node); - Filter get filter; + Selector get filter; } class Ready implements State { Ready(this.filter); @override - final Filter filter; + final Selector filter; @override State process(Node node) { if (node.value == '[') { - return Ready(filter.then(Index(int.parse(node.children.first.value)))); + return Ready(filter.then(_brackets(node.children))); } if (node.value == '.') { return AwaitingField(filter); } throw StateError('Got ${node.value} in $this'); } + + Selector _brackets(List nodes) { + if (nodes.length == 1) { + final node = nodes.single; + final val = node.value; + if (val == '*') return AllInArray(); + if (node.isNumber) return Index(int.parse(nodes.first.value)); + } + throw StateError('Unexpected bracket expression'); + } } class AwaitingField implements State { AwaitingField(this.filter); @override - final Filter filter; + final Selector filter; @override State process(Node node) { diff --git a/lib/src/tokenize.dart b/lib/src/tokenize.dart index e8ce9e8..1628b2e 100644 --- a/lib/src/tokenize.dart +++ b/lib/src/tokenize.dart @@ -1,3 +1,4 @@ +/// Parses a JSONPath expression into a list of tokens List tokenize(String expr) { final tokens = []; var pos = 0; diff --git a/pubspec.yaml b/pubspec.yaml index aa36b40..5890edf 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,10 +1,11 @@ name: jessie -version: 0.0.0+dev.0 -homepage: https://github.com/f3ath/jessie +version: 0.0.0+dev.1 description: JSONPath for Dart -environment: - sdk: ">=2.8.0 <3.0.0" +homepage: "https://github.com/f3ath/jessie" dev_dependencies: - test: ^1.9.0 pedantic: ^1.9.0 + test: ^1.9.0 + +environment: + sdk: ">=2.8.0 <3.0.0" diff --git a/test/json_path_test.dart b/test/json_path_test.dart index 7f0ecb2..e6b60a3 100644 --- a/test/json_path_test.dart +++ b/test/json_path_test.dart @@ -5,34 +5,44 @@ import 'package:jessie/jessie.dart'; import 'package:test/test.dart'; void main() { - final store = jsonDecode(File('test/store.json').readAsStringSync()); + final json = jsonDecode(File('test/store.json').readAsStringSync()); group('Basic expressions', () { test('Empty', () { final path = JsonPath(''); expect(path.toString(), r'$'); - expect(path.filter(store).single.value, store); - expect(path.filter(store).single.path, r'$'); + expect(path.select(json).single.value, json); + expect(path.select(json).single.path, r'$'); }); test('Only root', () { final path = JsonPath(r'$'); expect(path.toString(), r'$'); - expect(path.filter(store).single.value, store); - expect(path.filter(store).single.path, r'$'); + expect(path.select(json).single.value, json); + expect(path.select(json).single.path, r'$'); }); test('Single field', () { final path = JsonPath(r'$.store'); expect(path.toString(), r"$['store']"); - expect(path.filter(store).single.value, store['store']); - expect(path.filter(store).single.path, r"$['store']"); + expect(path.select(json).single.value, json['store']); + expect(path.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.filter(store).single.value, 'Sayings of the Century'); - expect(path.filter(store).single.path, 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']"); + }); + + 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]"); }); }); }