diff --git a/lib/jessie.dart b/lib/jessie.dart index 543256b..82130dd 100644 --- a/lib/jessie.dart +++ b/lib/jessie.dart @@ -2,5 +2,7 @@ library jessie; export 'package:jessie/src/field.dart'; +export 'package:jessie/src/filter.dart'; export 'package:jessie/src/index.dart'; -export 'package:jessie/src/json_path.dart'; +export 'package:jessie/src/neutral.dart'; +export 'package:jessie/src/tokenizer.dart'; diff --git a/lib/src/field.dart b/lib/src/field.dart index 8c2c791..e24334d 100644 --- a/lib/src/field.dart +++ b/lib/src/field.dart @@ -1,6 +1,6 @@ -import 'package:jessie/src/json_path.dart'; +import 'package:jessie/src/filter.dart'; -class Field extends JsonPath { +class Field extends Filter { Field(this.name); final String name; diff --git a/lib/src/json_path.dart b/lib/src/filter.dart similarity index 53% rename from lib/src/json_path.dart rename to lib/src/filter.dart index 2b5bb79..733cbc8 100644 --- a/lib/src/json_path.dart +++ b/lib/src/filter.dart @@ -1,4 +1,6 @@ -abstract class JsonPath { +abstract class Filter { + const Filter(); + /// Applies this JSONPath to the [nodes] Iterable call(Iterable nodes); @@ -7,22 +9,18 @@ abstract class JsonPath { String toString(); /// A shortcut for `then()` - JsonPath operator |(JsonPath other) => then(other); + Filter operator |(Filter other) => then(other); /// Combines this expression with the [other] - JsonPath then(JsonPath other) => _Chain(this, other); - - /// Filters the given nodes. - /// Returns an Iterable of all elements found - Iterable filter(dynamic node) => call([node]); + Filter then(Filter other) => _Chain(this, other); } -class _Chain extends JsonPath { +class _Chain extends Filter { _Chain(this.first, this.second); - final JsonPath first; + final Filter first; - final JsonPath second; + final Filter second; @override Iterable call(Iterable nodes) => second(first(nodes)); diff --git a/lib/src/index.dart b/lib/src/index.dart index 2a06054..a571f51 100644 --- a/lib/src/index.dart +++ b/lib/src/index.dart @@ -1,6 +1,6 @@ -import 'package:jessie/src/json_path.dart'; +import 'package:jessie/src/filter.dart'; -class Index extends JsonPath { +class Index extends Filter { Index(this.index); final int index; diff --git a/lib/src/neutral.dart b/lib/src/neutral.dart new file mode 100644 index 0000000..60dc870 --- /dev/null +++ b/lib/src/neutral.dart @@ -0,0 +1,11 @@ +import 'package:jessie/jessie.dart'; + +class Neutral extends Filter { + const Neutral(); + + @override + Iterable call(Iterable nodes) => nodes; + + @override + String toString() => r'$'; +} diff --git a/lib/src/tokenizer.dart b/lib/src/tokenizer.dart new file mode 100644 index 0000000..d715a1c --- /dev/null +++ b/lib/src/tokenizer.dart @@ -0,0 +1,129 @@ +List tokenize(String expression) { + final tokens = []; + var exp = expression; + while (exp.isNotEmpty) { + switch (exp[0]) { + case r'$': + tokens.add(const Root()); + exp = exp.substring(1); + break; + case '*': + tokens.add(const Asterisk()); + exp = exp.substring(1); + break; + case '[': + tokens.add(const LeftBracket()); + exp = exp.substring(1); + break; + case ']': + tokens.add(const RightBracket()); + exp = exp.substring(1); + break; + case '.': + if (exp.length > 1 && exp[1] == '.') { + tokens.add(const DoublePeriod()); + exp = exp.substring(2); + } else { + tokens.add(const Period()); + exp = exp.substring(1); + } + break; + default: + if (exp.startsWith(_digit)) { + final num = _allDigits.firstMatch(exp).group(0); + tokens.add(Number(int.parse(num))); + exp = exp.substring(num.length); + } else if (exp.startsWith(_char)) { + final text = _allAlphanumeric.firstMatch(exp).group(0); + tokens.add(Name(text)); + exp = exp.substring(text.length); + } else { + throw 'Can not tokenize "$exp"'; + } + } + } + return tokens; +} + +final _char = RegExp(r'[a-zA-Z]'); +final _allAlphanumeric = RegExp(r'[a-zA-Z][a-zA-Z0-9]+'); +final _digit = RegExp(r'\d'); +final _allDigits = RegExp(r'\d+'); + +abstract class Token {} + +class Root implements Token { + const Root(); + + @override + String toString() => r'$'; +} + +class Period implements Token { + const Period(); + + @override + String toString() => '.'; +} + +class DoublePeriod implements Token { + const DoublePeriod(); + + @override + String toString() => '..'; +} + +class Name implements Token { + const Name(this.value); + + final String value; + + @override + String toString() => value; +} + +class Number implements Token { + const Number(this.value); + + final int value; + + @override + String toString() => value.toString(); +} + +class Asterisk implements Token { + const Asterisk(); + + @override + String toString() => '*'; +} + +class LeftBracket implements Token { + const LeftBracket(); + + @override + String toString() => '['; +} + +class RightBracket implements Token { + const RightBracket(); + + @override + String toString() => ']'; +} + +//class LeftParenthesis implements Token { +// const LeftParenthesis(); +//} +// +//class RightParenthesis implements Token { +// const RightParenthesis(); +//} +// +//class Colon implements Token { +// const Colon(); +//} +// +//class Comma implements Token { +// const Comma(); +//} diff --git a/test/functional_test.dart b/test/functional_test.dart index cc9ca3e..0996551 100644 --- a/test/functional_test.dart +++ b/test/functional_test.dart @@ -7,12 +7,43 @@ import 'package:test/test.dart'; void main() { final store = jsonDecode(File('test/store.json').readAsStringSync()); group('Basic', () { - final path = Field('store') | Field('book') | Index(0) | Field('title'); - test('filtering', () { + final path = JsonPath('store.book[0].title'); + test('Filtering', () { expect(path.filter(store).single, 'Sayings of the Century'); }); test('toString()', () { - expect(path.toString(), "['store']['book'][0]['title']"); + expect(path.toString(), r"$['store']['book'][0]['title']"); }); }); + + group('Tokenizer', () { + test('Basic', () { + expect(tokenize('store.book[0].title').join(), 'store.book[0].title'); + }); + }); +} + +class JsonPath { + factory JsonPath(String path) { + final tokens = tokenize(path); + if (tokens.isNotEmpty && tokens.first == const Root()) { + tokens.removeAt(0); + } + + var filter = const Neutral(); + + return JsonPath._( + Neutral() | Field('store') | Field('book') | Index(0) | Field('title')); + } + + JsonPath._(this._filter); + + final Filter _filter; + + /// Filters the given [json]. + /// Returns an Iterable of all elements found + Iterable filter(json) => _filter.call([json]); + + @override + String toString() => _filter.toString(); }