Skip to content

Commit

Permalink
More expressions (#25)
Browse files Browse the repository at this point in the history
  • Loading branch information
f3ath authored Dec 19, 2021
1 parent 50f96a7 commit 4ac7cf5
Show file tree
Hide file tree
Showing 30 changed files with 552 additions and 176 deletions.
11 changes: 5 additions & 6 deletions .github/workflows/dart.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,9 @@ on:

jobs:
build:

runs-on: ubuntu-latest

container:
image: google/dart:beta

steps:
- uses: actions/checkout@v2
- name: Update submodules
Expand All @@ -21,9 +18,11 @@ jobs:
run: dart --version
- name: Install dependencies
run: dart pub get
- name: Format
run: dartfmt --dry-run --set-exit-if-changed lib test example
- name: Formatter
run: dart format --output none --set-exit-if-changed example lib test
- name: Analyzer
run: dart analyze --fatal-infos --fatal-warnings
- name: Tests
run: dart run test_coverage --no-badge --print-test-output --min-coverage 100
run: dart test --coverage=coverage
- name: Coverage
run: dart run coverage:format_coverage -l -c -i coverage --report-on=lib --packages=.packages | dart run check_coverage:check_coverage
8 changes: 6 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,12 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]
## 0.3.1 - 2021-12-18
### Added
- Filtering expressions

### Changed
- Require dart 2.15

## [0.3.0] - 2021-02-18
### Added
Expand Down Expand Up @@ -92,7 +97,6 @@ Previously, no modification would be made and no errors/exceptions thrown.
### Added
- Basic design draft

[Unreleased]: https://github.com/f3ath/jessie/compare/0.3.0...HEAD
[0.3.0]: https://github.com/f3ath/jessie/compare/0.2.0...0.3.0
[0.2.0]: https://github.com/f3ath/jessie/compare/0.1.2...0.2.0
[0.1.2]: https://github.com/f3ath/jessie/compare/0.1.1...0.1.2
Expand Down
31 changes: 25 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,17 +63,36 @@ void main() {
}
```

## Features and limitations
Generally, this library tries to mimic the [reference implementations], except for the filtering.
Evaluated filtering expressions (e.g. `$..book[?(@.price<10)]`) support is limited.
Instead, use the callback-kind of filters.
## Limitations
This library is intended to match the [original implementations], although filtering expressions (like `$..book[?(@.price < 10)]`)
support is limited and may not always produce the results that you expect. The reason is the
difference between the type systems of Dart and JavaScript/PHP. Dart is strictly typed and does not support `eval()`,
so the expressions have to be parsed and evaluated by the library itself. Implementing complex logic this way would
create a performance overhead which I prefer to avoid.

## Callback filters
If there is a real need for complex filtering, you may use Dart-native callbacks. The syntax, _which is of course my own
invention and has nothing to do with the "official" JSONPath_, is the following:
```dart
/// Select all elements with price under 20
/// Selects all elements with price under 20
final path = JsonPath(r'$.store..[?discounted]', filters: {
'discounted': (match) =>
match.value is Map && match.value['price'] is num && match.value['price'] < 20
});
```
The filtering callbacks may be passed to the `JSONPath` constructor or to the `.read()` method. The callback name
must be specified in the square brackets and prefixed by `?`. It also must be alphanumeric.

## Data manipulation
Each `JsonPathMatch` produced by the `.read()` method contains the `.pointer` property which is a valid [JSON Pointer]
and can be used to write/append/remove the referenced value. If you're looking for data manipulation only,
take a look at this [JSON Pointer implementation].

## References
- [Standard development](https://github.com/ietf-wg-jsonpath/draft-ietf-jsonpath-base)
- [Feature comparison matrix](https://cburgmer.github.io/json-path-comparison/)

[JSONPath]: https://goessner.net/articles/JsonPath/
[reference implementations]: https://goessner.net/articles/JsonPath/index.html#e4
[JSON Pointer]: https://datatracker.ietf.org/doc/html/rfc6901
[JSON Pointer implementation]: https://pub.dev/packages/rfc_6901
[original implementations]: https://goessner.net/articles/JsonPath/index.html#e4
2 changes: 1 addition & 1 deletion analysis_options.yaml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
include: package:pedantic/analysis_options.yaml
include: package:lints/recommended.yaml
linter:
rules:
- sort_constructors_first
Expand Down
17 changes: 17 additions & 0 deletions example/example.dart
Original file line number Diff line number Diff line change
Expand Up @@ -57,4 +57,21 @@ void main() {
.read(document)
.map((match) => '${match.pointer}:\t${match.value}')
.forEach(print);

print('Books under 10:');
JsonPath(r'$.store.book[?(@.price < 10)].title')
.read(document)
.map((match) => '${match.pointer}:\t${match.value}')
.forEach(print);

print("Book with letter 'a' in the author's name:");
JsonPath(r'$.store.book[?author].title', filters: {
'author': (match) {
final author = match.value['author'];
return author is String && author.contains('a');
}
})
.read(document)
.map((match) => '${match.pointer}:\t${match.value}')
.forEach(print);
}
1 change: 1 addition & 0 deletions lib/json_path.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
/// JSONPath for Dart
library json_path;

export 'package:json_path/src/algebra.dart';
export 'package:json_path/src/filter_not_found.dart';
export 'package:json_path/src/json_path.dart';
export 'package:json_path/src/json_path_match.dart';
Expand Down
102 changes: 102 additions & 0 deletions lib/src/algebra.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
/// Evaluation rules used in expressions like `$[?(@.foo > 2)]`.
/// Allows users to implement custom evaluation rules to emulate behavior
/// in other programming languages, like JavaScript.
abstract class Algebra {
/// A set of rules with strictly typed operations.
/// Throws [TypeError] when the operation is not applicable to operand types.
static const strict = _Strict();

/// A relaxed set of rules allowing some operations on not fully compatible types.
/// E.g. `1 < "3"` would return false instead of throwing a [TypeError].
static const relaxed = _Relaxed();

/// True if [a] equals [b].
bool eq(dynamic a, dynamic b);

/// True if [a] is not equal to [b].
bool ne(dynamic a, dynamic b);

/// True if [a] is strictly less than [b].
bool lt(dynamic a, dynamic b);

/// True if [a] is less or equal to [b].
bool le(dynamic a, dynamic b);

/// True if [a] is strictly greater than [b].
bool gt(dynamic a, dynamic b);

/// True if [a] is greater or equal to [b].
bool ge(dynamic a, dynamic b);

/// True if both [a] and [b] are truthy.
bool and(dynamic a, dynamic b);

/// True if either [a] or [b] are truthy.
bool or(dynamic a, dynamic b);

/// Casts the [val] to bool.
bool isTruthy(dynamic val);
}

class _Strict implements Algebra {
const _Strict();

@override
bool isTruthy(dynamic val) => val;

@override
bool eq(a, b) => a == b;

@override
bool ge(a, b) => a >= b;

@override
bool gt(a, b) => a > b;

@override
bool le(a, b) => a <= b;

@override
bool lt(a, b) => a < b;

@override
bool ne(a, b) => a != b;

@override
bool and(dynamic a, dynamic b) => isTruthy(a) && isTruthy(b);

@override
bool or(dynamic a, dynamic b) => isTruthy(a) || isTruthy(b);
}

class _Relaxed extends _Strict {
const _Relaxed();

@override
bool isTruthy(dynamic val) =>
val == true ||
val is List ||
val is Map ||
(val is num && val != 0) ||
(val is String && val.isNotEmpty);

@override
bool ge(a, b) =>
(a is String && b is String && a.compareTo(b) >= 0) ||
(a is num && b is num && a >= b);

@override
bool gt(a, b) =>
(a is String && b is String && a.compareTo(b) > 0) ||
(a is num && b is num && a > b);

@override
bool le(a, b) =>
(a is String && b is String && a.compareTo(b) <= 0) ||
(a is num && b is num && a <= b);

@override
bool lt(a, b) =>
(a is String && b is String && a.compareTo(b) < 0) ||
(a is num && b is num && a < b);
}
2 changes: 1 addition & 1 deletion lib/src/child_match.dart
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ class ChildMatch implements JsonPathMatch {

/// The value
@override
final value;
final dynamic value;

/// JSONPath to this match
@override
Expand Down
69 changes: 53 additions & 16 deletions lib/src/grammar/expression.dart
Original file line number Diff line number Diff line change
@@ -1,25 +1,27 @@
import 'package:json_path/json_path.dart';
import 'package:json_path/src/grammar/integer.dart';
import 'package:json_path/src/grammar/strings.dart';
import 'package:json_path/src/selector/expression_filter.dart';
import 'package:petitparser/petitparser.dart';
import 'package:json_path/src/it.dart' as it;

Parser<Eval> _build() {
final _true = string('true').map((_) => true);
final _false = string('false').map((_) => false);
final _null = string('null').map((_) => null);
Parser<Predicate> _build() {
final _true = string('true').map(it.to(true));
final _false = string('false').map(it.to(false));
final _null = string('null').map(it.to(null));

final _literal = (_false |
_true |
_null |
integer |
doubleQuotedString |
singleQuotedString)
.map<Eval>((value) => (match) => value);
.map((value) => (match) => value);

final _index = (char('[') &
(integer | doubleQuotedString | singleQuotedString) &
char(']'))
.map((value) => value[1])
.map((_) => _[1])
.map((key) => (v) {
if (key is int && v is List && key < v.length && key >= 0) {
return v[key];
Expand All @@ -28,8 +30,7 @@ Parser<Eval> _build() {
}
});

final _dotName = (char('.') & dotString).map((value) => (v) {
final key = value.last;
final _dotName = (char('.') & dotString).map(it.last).map((key) => (v) {
if (v is Map && v.containsKey(key)) {
return v[key];
}
Expand All @@ -39,23 +40,59 @@ Parser<Eval> _build() {
.plus()
.map(
(value) => value.reduce((value, element) => (v) => element(value(v))))
.map<Eval>((value) => (match) => value(match.value));
.map((value) => (match) => value(match.value));

final _node = (char('@') & _nodeFilter).map((value) => value.last);
final _currentObject = char('@').map((_) => (match) => match.value);

final _node =
(_currentObject & _nodeFilter.optional()).map(it.lastWhere(it.isNotNull));

final _term = undefined();

final _parens =
(char('(').trim() & _term & char(')').trim()).map((value) => value[1]);
(char('(').trim() & _term & char(')').trim()).map((_) => _[1]);

final _operand = _parens | _literal | _node;

final _eq = string('==')
.map<_BinaryOp>(it.to((algebra, left, right) => algebra.eq(left, right)));

final _ne = string('!=')
.map<_BinaryOp>(it.to((algebra, left, tight) => algebra.ne(left, tight)));

final _ge = string('>=')
.map<_BinaryOp>(it.to((algebra, left, right) => algebra.ge(left, right)));

final _gt = string('>')
.map<_BinaryOp>(it.to((algebra, left, right) => algebra.gt(left, right)));

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

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

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

return (string('?(') & _term & char(')')).map((value) => value[1]);
final _and = string('&&').map<_BinaryOp>(
it.to((algebra, left, right) => algebra.and(left, right)));

final _binaryOperator = _eq | _ne | _ge | _gt | _le | _lt | _or | _and;

final _expression = (_operand & _binaryOperator.trim() & _operand)
.map((value) => (JsonPathMatch match) {
final op = value[1];
return op(
match.context.algebra, value.first(match), value.last(match));
});

_term.set(_expression | _operand);

return (string('?(') & _term & char(')')).map((_) => _[1]).map<Predicate>(
(eval) => (match) => match.context.algebra.isTruthy(eval(match)));
}

typedef _BinaryOp = bool Function(Algebra algebra, dynamic left, dynamic right);

final expression = _build();
Loading

0 comments on commit 4ac7cf5

Please sign in to comment.