Skip to content

Commit

Permalink
Expressions (#21)
Browse files Browse the repository at this point in the history
  • Loading branch information
f3ath authored Feb 19, 2021
1 parent 7c9399e commit 50f96a7
Show file tree
Hide file tree
Showing 9 changed files with 88 additions and 59 deletions.
14 changes: 12 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,16 +1,25 @@
# Changelog
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.0] - 2021-02-18
### Added
- `JsonPathMatch.context` contains the matching context. It is intended to be used in named filters.
- `JsonPathMatch.parent` contains the parent match.
- `JsonPathMatch.pointer` contains the RFC 6901 JSON Pointer to the match.
- Very basic support for evaluated expressions

### Changed
- Named filters argument renamed from `filter` to `filters`
- Named filters can now be passed to the `read()` method.
- Named filters callback now accepts the entire `JsonPathMatch` object, not just the value.

### Removed
- The `set()` method. The intention is to allow modifications via JSON Pointer in the future.
- The `set()` method. Use the `pointer` property instead.

## [0.2.0] - 2020-09-07
### Added
Expand Down Expand Up @@ -83,7 +92,8 @@ Previously, no modification would be made and no errors/exceptions thrown.
### Added
- Basic design draft

[Unreleased]: https://github.com/f3ath/jessie/compare/0.2.0...HEAD
[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
[0.1.1]: https://github.com/f3ath/jessie/compare/0.1.0...0.1.1
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ void main() {

## Features and limitations
Generally, this library tries to mimic the [reference implementations], except for the filtering.
Evaluated filtering expressions like `$..book[?(@.price<10)]` are NOT yet supported.
Evaluated filtering expressions (e.g. `$..book[?(@.price<10)]`) support is limited.
Instead, use the callback-kind of filters.
```dart
/// Select all elements with price under 20
Expand Down
44 changes: 25 additions & 19 deletions lib/src/grammar/expression.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,50 +4,56 @@ import 'package:json_path/src/selector/expression_filter.dart';
import 'package:petitparser/petitparser.dart';

Parser<Eval> _build() {
final _true = string('true').trim().map<bool>((value) => true);
final _false = string('false').trim().map<bool>((value) => false);

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

final _term = undefined();

final _parens = (char('(').trim() & _term & char(')').trim())
.map((value) => (match) => value[1](match));
final _true = string('true').map((_) => true);
final _false = string('false').map((_) => false);
final _null = string('null').map((_) => null);

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

final _index = (char('[') &
(integer | doubleQuotedString | singleQuotedString) &
char(']'))
.map((value) => value[1])
.map<Eval>((value) => (match) {
final key = value;
final v = match.value;
.map((key) => (v) {
if (key is int && v is List && key < v.length && key >= 0) {
return v[key];
} else if (key is String && v is Map && v.containsKey(key)) {
return v[key];
}
});

final _dotName = (char('.') & dotString).map((value) => (match) {
final _dotName = (char('.') & dotString).map((value) => (v) {
final key = value.last;
final v = match.value;
if (v is Map && v.containsKey(key)) {
return v[key];
}
});

final _nodeMapper = _index | _dotName;
final _nodeFilter = (_index | _dotName)
.plus()
.map(
(value) => value.reduce((value, element) => (v) => element(value(v))))
.map<Eval>((value) => (match) => value(match.value));

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

final _term = undefined();

final _node = (char('@') & _nodeMapper).map<Eval>((value) => value.last);
final _parens =
(char('(').trim() & _term & char(')').trim()).map((value) => value[1]);

final _comparable = _parens | _literal | _node;

final _comparison = (_comparable & string('==').trim() & _comparable)
.map((value) => (match) => value.first(match) == value.last(match));

_term.set(_comparison | _parens | _literal | _node);
_term.set(_comparison | _comparable);

return (string('?(') & _term & char(')')).map((value) => value[1]);
}
Expand Down
13 changes: 11 additions & 2 deletions lib/src/matching_context.dart
Original file line number Diff line number Diff line change
@@ -1,8 +1,17 @@
import 'package:json_path/src/filter_not_found.dart';
import 'package:json_path/src/json_path_match.dart';

class MatchingContext {
const MatchingContext(this.filters);
const MatchingContext(this._filters);

/// Named callback filters
final Map<String, CallbackFilter> filters;
final Map<String, CallbackFilter> _filters;

CallbackFilter getFilter(String name) {
final filter = _filters[name];
if (filter == null) {
throw FilterNotFound('Callback filter "$name" not found');
}
return filter;
}
}
15 changes: 4 additions & 11 deletions lib/src/selector/callback_filter.dart
Original file line number Diff line number Diff line change
@@ -1,19 +1,12 @@
import 'package:json_path/src/filter_not_found.dart';
import 'package:json_path/src/json_path_match.dart';
import 'package:json_path/src/selector.dart';
import 'package:json_path/src/selector/wildcard.dart';

class CallbackFilter implements Selector {
CallbackFilter(this.name);
class CallbackFilter extends Wildcard {
const CallbackFilter(this.name);

final String name;

@override
Iterable<JsonPathMatch> apply(JsonPathMatch match) sync* {
final filter = match.context.filters[name];
if (filter == null) {
throw FilterNotFound('Callback filter "$name" not found');
}
yield* const Wildcard().apply(match).where(filter);
}
Iterable<JsonPathMatch> apply(JsonPathMatch match) =>
super.apply(match).where(match.context.getFilter(name));
}
18 changes: 8 additions & 10 deletions lib/src/selector/expression_filter.dart
Original file line number Diff line number Diff line change
@@ -1,21 +1,19 @@
import 'package:json_path/src/json_path_match.dart';
import 'package:json_path/src/selector.dart';
import 'package:json_path/src/selector/wildcard.dart';

class ExpressionFilter implements Selector {
class ExpressionFilter extends Wildcard {
ExpressionFilter(this.eval);

final Eval eval;

@override
Iterable<JsonPathMatch> apply(JsonPathMatch match) sync* {
yield* const Wildcard().apply(match).where((match) {
final val = eval(match);
return (val == true ||
(val is num && val != 0) ||
(val is String && val.isNotEmpty));
});
}
Iterable<JsonPathMatch> apply(JsonPathMatch match) =>
super.apply(match).where((match) {
final val = eval(match);
return (val == true ||
(val is num && val != 0) ||
(val is String && val.isNotEmpty));
});
}

typedef Eval<T> = T Function(JsonPathMatch match);
10 changes: 5 additions & 5 deletions lib/src/selector/sequence.dart
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
import 'package:json_path/src/json_path_match.dart';
import 'package:json_path/src/selector.dart';

typedef Filter = Iterable<JsonPathMatch> Function(
Iterable<JsonPathMatch> matches);

class Sequence implements Selector {
Sequence(Iterable<Selector> selectors)
: _filter = selectors.fold<Filter>(
: _filter = selectors.fold<_Filter>(
(_) => _,
(filter, selector) => (matches) =>
filter(matches).map(selector.apply).expand((_) => _));

final Filter _filter;
final _Filter _filter;

@override
Iterable<JsonPathMatch> apply(JsonPathMatch match) => _filter([match]);
}

typedef _Filter = Iterable<JsonPathMatch> Function(
Iterable<JsonPathMatch> matches);
15 changes: 7 additions & 8 deletions pubspec.yaml
Original file line number Diff line number Diff line change
@@ -1,19 +1,18 @@
name: json_path
version: 0.3.0-nullsafety.3
description: Implementation of JSONPath expressions like "$.store.book[2].price". Can read and set values in parsed JSON objects.
version: 0.3.0
description: Implementation of JSONPath expressions like "$.store.book[2].price". Reads and writes values in parsed JSON objects.
homepage: "https://github.com/f3ath/jessie"

environment:
sdk: '>=2.12.0-29 <3.0.0'

dependencies:
rfc_6901: ^0.0.0-nullsafety.7
petitparser: ^4.0.0-nullsafety
rfc_6901: ^0.1.0
petitparser: ^4.0.0

dev_dependencies:
pedantic: ^1.10.0-nullsafety
test: ^1.16.0-nullsafety
pedantic: ^1.10.0
test: ^1.16.2
test_coverage: ^0.5.0
path: ^1.8.0-nullsafety
yaml: ^3.0.0-nullsafety
path: ^1.8.0

16 changes: 15 additions & 1 deletion test/cases/expressions.json
Original file line number Diff line number Diff line change
Expand Up @@ -93,11 +93,25 @@
"pointers": ["/1"]
}, {
"name": "object child, dot-notation",
"selector": "$[?(@.key == 'b')]",
"selector": "$[?(@.key=='b')]",
"document": [{"key": "a"}, {"key": "b"}, {}],
"values": [{"key": "b"}],
"paths": ["$[1]"],
"pointers": ["/1"]
}, {
"name": "object child, combined",
"selector": "$[?(@.foo['bar'] == 'b')]",
"document": [{"foo": {"bar": "a"}}, {"foo": {"bar": "b"}}, {"foo": {"moo": 42}}, {}],
"values": [{"foo": {"bar": "b"}}],
"paths": ["$[1]"],
"pointers": ["/1"]
}, {
"name": "equal props",
"selector": "$[?(@.foo == @.bar)]",
"document": [{"foo": 1, "bar": 2}, {"foo": 42, "bar": 42}, {"foo": 1, "bro": 1}, {}],
"values": [{"foo": 42, "bar": 42}, {}],
"paths": ["$[1]", "$[3]"],
"pointers": ["/1", "/3"]
}
]
}

0 comments on commit 50f96a7

Please sign in to comment.