Skip to content

Commit 4ac7cf5

Browse files
authored
More expressions (#25)
1 parent 50f96a7 commit 4ac7cf5

30 files changed

+552
-176
lines changed

Diff for: .github/workflows/dart.yml

+5-6
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,9 @@ on:
77

88
jobs:
99
build:
10-
1110
runs-on: ubuntu-latest
12-
1311
container:
1412
image: google/dart:beta
15-
1613
steps:
1714
- uses: actions/checkout@v2
1815
- name: Update submodules
@@ -21,9 +18,11 @@ jobs:
2118
run: dart --version
2219
- name: Install dependencies
2320
run: dart pub get
24-
- name: Format
25-
run: dartfmt --dry-run --set-exit-if-changed lib test example
21+
- name: Formatter
22+
run: dart format --output none --set-exit-if-changed example lib test
2623
- name: Analyzer
2724
run: dart analyze --fatal-infos --fatal-warnings
2825
- name: Tests
29-
run: dart run test_coverage --no-badge --print-test-output --min-coverage 100
26+
run: dart test --coverage=coverage
27+
- name: Coverage
28+
run: dart run coverage:format_coverage -l -c -i coverage --report-on=lib --packages=.packages | dart run check_coverage:check_coverage

Diff for: CHANGELOG.md

+6-2
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,12 @@ All notable changes to this project will be documented in this file.
44
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
55
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
66

7-
## [Unreleased]
7+
## 0.3.1 - 2021-12-18
8+
### Added
9+
- Filtering expressions
10+
11+
### Changed
12+
- Require dart 2.15
813

914
## [0.3.0] - 2021-02-18
1015
### Added
@@ -92,7 +97,6 @@ Previously, no modification would be made and no errors/exceptions thrown.
9297
### Added
9398
- Basic design draft
9499

95-
[Unreleased]: https://github.com/f3ath/jessie/compare/0.3.0...HEAD
96100
[0.3.0]: https://github.com/f3ath/jessie/compare/0.2.0...0.3.0
97101
[0.2.0]: https://github.com/f3ath/jessie/compare/0.1.2...0.2.0
98102
[0.1.2]: https://github.com/f3ath/jessie/compare/0.1.1...0.1.2

Diff for: README.md

+25-6
Original file line numberDiff line numberDiff line change
@@ -63,17 +63,36 @@ void main() {
6363
}
6464
```
6565

66-
## Features and limitations
67-
Generally, this library tries to mimic the [reference implementations], except for the filtering.
68-
Evaluated filtering expressions (e.g. `$..book[?(@.price<10)]`) support is limited.
69-
Instead, use the callback-kind of filters.
66+
## Limitations
67+
This library is intended to match the [original implementations], although filtering expressions (like `$..book[?(@.price < 10)]`)
68+
support is limited and may not always produce the results that you expect. The reason is the
69+
difference between the type systems of Dart and JavaScript/PHP. Dart is strictly typed and does not support `eval()`,
70+
so the expressions have to be parsed and evaluated by the library itself. Implementing complex logic this way would
71+
create a performance overhead which I prefer to avoid.
72+
73+
## Callback filters
74+
If there is a real need for complex filtering, you may use Dart-native callbacks. The syntax, _which is of course my own
75+
invention and has nothing to do with the "official" JSONPath_, is the following:
7076
```dart
71-
/// Select all elements with price under 20
77+
/// Selects all elements with price under 20
7278
final path = JsonPath(r'$.store..[?discounted]', filters: {
7379
'discounted': (match) =>
7480
match.value is Map && match.value['price'] is num && match.value['price'] < 20
7581
});
7682
```
83+
The filtering callbacks may be passed to the `JSONPath` constructor or to the `.read()` method. The callback name
84+
must be specified in the square brackets and prefixed by `?`. It also must be alphanumeric.
85+
86+
## Data manipulation
87+
Each `JsonPathMatch` produced by the `.read()` method contains the `.pointer` property which is a valid [JSON Pointer]
88+
and can be used to write/append/remove the referenced value. If you're looking for data manipulation only,
89+
take a look at this [JSON Pointer implementation].
90+
91+
## References
92+
- [Standard development](https://github.com/ietf-wg-jsonpath/draft-ietf-jsonpath-base)
93+
- [Feature comparison matrix](https://cburgmer.github.io/json-path-comparison/)
7794

7895
[JSONPath]: https://goessner.net/articles/JsonPath/
79-
[reference implementations]: https://goessner.net/articles/JsonPath/index.html#e4
96+
[JSON Pointer]: https://datatracker.ietf.org/doc/html/rfc6901
97+
[JSON Pointer implementation]: https://pub.dev/packages/rfc_6901
98+
[original implementations]: https://goessner.net/articles/JsonPath/index.html#e4

Diff for: analysis_options.yaml

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
include: package:pedantic/analysis_options.yaml
1+
include: package:lints/recommended.yaml
22
linter:
33
rules:
44
- sort_constructors_first

Diff for: example/example.dart

+17
Original file line numberDiff line numberDiff line change
@@ -57,4 +57,21 @@ void main() {
5757
.read(document)
5858
.map((match) => '${match.pointer}:\t${match.value}')
5959
.forEach(print);
60+
61+
print('Books under 10:');
62+
JsonPath(r'$.store.book[?(@.price < 10)].title')
63+
.read(document)
64+
.map((match) => '${match.pointer}:\t${match.value}')
65+
.forEach(print);
66+
67+
print("Book with letter 'a' in the author's name:");
68+
JsonPath(r'$.store.book[?author].title', filters: {
69+
'author': (match) {
70+
final author = match.value['author'];
71+
return author is String && author.contains('a');
72+
}
73+
})
74+
.read(document)
75+
.map((match) => '${match.pointer}:\t${match.value}')
76+
.forEach(print);
6077
}

Diff for: lib/json_path.dart

+1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
/// JSONPath for Dart
22
library json_path;
33

4+
export 'package:json_path/src/algebra.dart';
45
export 'package:json_path/src/filter_not_found.dart';
56
export 'package:json_path/src/json_path.dart';
67
export 'package:json_path/src/json_path_match.dart';

Diff for: lib/src/algebra.dart

+102
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
/// Evaluation rules used in expressions like `$[?(@.foo > 2)]`.
2+
/// Allows users to implement custom evaluation rules to emulate behavior
3+
/// in other programming languages, like JavaScript.
4+
abstract class Algebra {
5+
/// A set of rules with strictly typed operations.
6+
/// Throws [TypeError] when the operation is not applicable to operand types.
7+
static const strict = _Strict();
8+
9+
/// A relaxed set of rules allowing some operations on not fully compatible types.
10+
/// E.g. `1 < "3"` would return false instead of throwing a [TypeError].
11+
static const relaxed = _Relaxed();
12+
13+
/// True if [a] equals [b].
14+
bool eq(dynamic a, dynamic b);
15+
16+
/// True if [a] is not equal to [b].
17+
bool ne(dynamic a, dynamic b);
18+
19+
/// True if [a] is strictly less than [b].
20+
bool lt(dynamic a, dynamic b);
21+
22+
/// True if [a] is less or equal to [b].
23+
bool le(dynamic a, dynamic b);
24+
25+
/// True if [a] is strictly greater than [b].
26+
bool gt(dynamic a, dynamic b);
27+
28+
/// True if [a] is greater or equal to [b].
29+
bool ge(dynamic a, dynamic b);
30+
31+
/// True if both [a] and [b] are truthy.
32+
bool and(dynamic a, dynamic b);
33+
34+
/// True if either [a] or [b] are truthy.
35+
bool or(dynamic a, dynamic b);
36+
37+
/// Casts the [val] to bool.
38+
bool isTruthy(dynamic val);
39+
}
40+
41+
class _Strict implements Algebra {
42+
const _Strict();
43+
44+
@override
45+
bool isTruthy(dynamic val) => val;
46+
47+
@override
48+
bool eq(a, b) => a == b;
49+
50+
@override
51+
bool ge(a, b) => a >= b;
52+
53+
@override
54+
bool gt(a, b) => a > b;
55+
56+
@override
57+
bool le(a, b) => a <= b;
58+
59+
@override
60+
bool lt(a, b) => a < b;
61+
62+
@override
63+
bool ne(a, b) => a != b;
64+
65+
@override
66+
bool and(dynamic a, dynamic b) => isTruthy(a) && isTruthy(b);
67+
68+
@override
69+
bool or(dynamic a, dynamic b) => isTruthy(a) || isTruthy(b);
70+
}
71+
72+
class _Relaxed extends _Strict {
73+
const _Relaxed();
74+
75+
@override
76+
bool isTruthy(dynamic val) =>
77+
val == true ||
78+
val is List ||
79+
val is Map ||
80+
(val is num && val != 0) ||
81+
(val is String && val.isNotEmpty);
82+
83+
@override
84+
bool ge(a, b) =>
85+
(a is String && b is String && a.compareTo(b) >= 0) ||
86+
(a is num && b is num && a >= b);
87+
88+
@override
89+
bool gt(a, b) =>
90+
(a is String && b is String && a.compareTo(b) > 0) ||
91+
(a is num && b is num && a > b);
92+
93+
@override
94+
bool le(a, b) =>
95+
(a is String && b is String && a.compareTo(b) <= 0) ||
96+
(a is num && b is num && a <= b);
97+
98+
@override
99+
bool lt(a, b) =>
100+
(a is String && b is String && a.compareTo(b) < 0) ||
101+
(a is num && b is num && a < b);
102+
}

Diff for: lib/src/child_match.dart

+1-1
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ class ChildMatch implements JsonPathMatch {
1919

2020
/// The value
2121
@override
22-
final value;
22+
final dynamic value;
2323

2424
/// JSONPath to this match
2525
@override

Diff for: lib/src/grammar/expression.dart

+53-16
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,27 @@
1+
import 'package:json_path/json_path.dart';
12
import 'package:json_path/src/grammar/integer.dart';
23
import 'package:json_path/src/grammar/strings.dart';
34
import 'package:json_path/src/selector/expression_filter.dart';
45
import 'package:petitparser/petitparser.dart';
6+
import 'package:json_path/src/it.dart' as it;
57

6-
Parser<Eval> _build() {
7-
final _true = string('true').map((_) => true);
8-
final _false = string('false').map((_) => false);
9-
final _null = string('null').map((_) => null);
8+
Parser<Predicate> _build() {
9+
final _true = string('true').map(it.to(true));
10+
final _false = string('false').map(it.to(false));
11+
final _null = string('null').map(it.to(null));
1012

1113
final _literal = (_false |
1214
_true |
1315
_null |
1416
integer |
1517
doubleQuotedString |
1618
singleQuotedString)
17-
.map<Eval>((value) => (match) => value);
19+
.map((value) => (match) => value);
1820

1921
final _index = (char('[') &
2022
(integer | doubleQuotedString | singleQuotedString) &
2123
char(']'))
22-
.map((value) => value[1])
24+
.map((_) => _[1])
2325
.map((key) => (v) {
2426
if (key is int && v is List && key < v.length && key >= 0) {
2527
return v[key];
@@ -28,8 +30,7 @@ Parser<Eval> _build() {
2830
}
2931
});
3032

31-
final _dotName = (char('.') & dotString).map((value) => (v) {
32-
final key = value.last;
33+
final _dotName = (char('.') & dotString).map(it.last).map((key) => (v) {
3334
if (v is Map && v.containsKey(key)) {
3435
return v[key];
3536
}
@@ -39,23 +40,59 @@ Parser<Eval> _build() {
3940
.plus()
4041
.map(
4142
(value) => value.reduce((value, element) => (v) => element(value(v))))
42-
.map<Eval>((value) => (match) => value(match.value));
43+
.map((value) => (match) => value(match.value));
4344

44-
final _node = (char('@') & _nodeFilter).map((value) => value.last);
45+
final _currentObject = char('@').map((_) => (match) => match.value);
46+
47+
final _node =
48+
(_currentObject & _nodeFilter.optional()).map(it.lastWhere(it.isNotNull));
4549

4650
final _term = undefined();
4751

4852
final _parens =
49-
(char('(').trim() & _term & char(')').trim()).map((value) => value[1]);
53+
(char('(').trim() & _term & char(')').trim()).map((_) => _[1]);
54+
55+
final _operand = _parens | _literal | _node;
56+
57+
final _eq = string('==')
58+
.map<_BinaryOp>(it.to((algebra, left, right) => algebra.eq(left, right)));
59+
60+
final _ne = string('!=')
61+
.map<_BinaryOp>(it.to((algebra, left, tight) => algebra.ne(left, tight)));
62+
63+
final _ge = string('>=')
64+
.map<_BinaryOp>(it.to((algebra, left, right) => algebra.ge(left, right)));
65+
66+
final _gt = string('>')
67+
.map<_BinaryOp>(it.to((algebra, left, right) => algebra.gt(left, right)));
5068

51-
final _comparable = _parens | _literal | _node;
69+
final _le = string('<=')
70+
.map<_BinaryOp>(it.to((algebra, left, right) => algebra.le(left, right)));
5271

53-
final _comparison = (_comparable & string('==').trim() & _comparable)
54-
.map((value) => (match) => value.first(match) == value.last(match));
72+
final _lt = string('<')
73+
.map<_BinaryOp>(it.to((algebra, left, right) => algebra.lt(left, right)));
5574

56-
_term.set(_comparison | _comparable);
75+
final _or = string('||')
76+
.map<_BinaryOp>(it.to((algebra, left, right) => algebra.or(left, right)));
5777

58-
return (string('?(') & _term & char(')')).map((value) => value[1]);
78+
final _and = string('&&').map<_BinaryOp>(
79+
it.to((algebra, left, right) => algebra.and(left, right)));
80+
81+
final _binaryOperator = _eq | _ne | _ge | _gt | _le | _lt | _or | _and;
82+
83+
final _expression = (_operand & _binaryOperator.trim() & _operand)
84+
.map((value) => (JsonPathMatch match) {
85+
final op = value[1];
86+
return op(
87+
match.context.algebra, value.first(match), value.last(match));
88+
});
89+
90+
_term.set(_expression | _operand);
91+
92+
return (string('?(') & _term & char(')')).map((_) => _[1]).map<Predicate>(
93+
(eval) => (match) => match.context.algebra.isTruthy(eval(match)));
5994
}
6095

96+
typedef _BinaryOp = bool Function(Algebra algebra, dynamic left, dynamic right);
97+
6198
final expression = _build();

0 commit comments

Comments
 (0)