Skip to content

Commit

Permalink
Mutations (#5)
Browse files Browse the repository at this point in the history
  • Loading branch information
f3ath authored Sep 6, 2020
1 parent f6a44ca commit 5dd5088
Show file tree
Hide file tree
Showing 34 changed files with 834 additions and 563 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/dart.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,4 @@ jobs:
- name: Analyzer
run: dartanalyzer --fatal-infos --fatal-warnings lib test example
- name: Tests
run: pub run test
run: pub run test_coverage --no-badge --print-test-output --min-coverage 100
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,8 @@ doc/api/
*.js_
*.js.deps
*.js.map

# test_coverage
test/.test_coverage.dart
coverage
coverage_badge.svg
11 changes: 10 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,12 @@
## [Unreleased]
## [0.1.0] - 2020-09-05
### Added
- JsonPath.set() method to alter the JSON object in a non-destructive way

### Changed
- **BREAKING!** `Result` renamed to `JsonPathMatch`
- **BREAKING!** `JsonPath.filter()` renamed to `read()`

## [0.0.2] - 2020-09-01
### Fixed
- Last element of array was not selected (regression #1)
Expand Down Expand Up @@ -41,7 +49,8 @@
### Added
- Basic design draft

[Unreleased]: https://github.com/f3ath/jessie/compare/0.0.2...HEAD
[Unreleased]: https://github.com/f3ath/jessie/compare/0.1.0...HEAD
[0.1.0]: https://github.com/f3ath/jessie/compare/0.0.2...0.1.0
[0.0.2]: https://github.com/f3ath/jessie/compare/0.0.1...0.0.2
[0.0.1]: https://github.com/f3ath/jessie/compare/0.0.0+dev.7...0.0.1
[0.0.0+dev.7]: https://github.com/f3ath/jessie/compare/0.0.0+dev.6...0.0.0+dev.7
Expand Down
34 changes: 31 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,17 +45,45 @@ void main() {
}
''');
final prices = JsonPath(r'$..price');
print('All prices in the store:');
/// The following code will print:
///
/// $['store']['book'][0]['price']: 8.95
/// $['store']['book'][1]['price']: 12.99
/// $['store']['book'][2]['price']: 8.99
/// $['store']['book'][3]['price']: 22.99
/// $['store']['bicycle']['price']: 19.95
JsonPath(r'$..price')
.filter(json)
prices
.read(json)
.map((result) => '${result.path}:\t${result.value}')
.forEach(print);
print('\n');
final bikeColor = JsonPath(r'$.store.bicycle.color');
print('A copy of the store with repainted bike:');
print(bikeColor.set(json, 'blue'));
print('\n');
print('Note, that the bike in the original store is still red:');
bikeColor
.read(json)
.map((result) => '${result.path}:\t${result.value}')
.forEach(print);
print('\n');
print('It is also possible to modify json in place:');
final someBooks = JsonPath(r'$.store.book[::2]');
someBooks.read(json).forEach((match) {
result.value['title'] = 'Banana';
});
print(json);
}
```
Expand Down Expand Up @@ -89,7 +117,7 @@ Instead, use the callback-kind of filters.
```dart
/// Select all elements with price under 20
JsonPath(r'$.store..[?discounted]', filter: {
'discounted': (e) => e is Map && e['price'] is num && e['price'] < 20
'discounted': (e) => e['price'] < 20
});
```

Expand Down
33 changes: 31 additions & 2 deletions example/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -42,15 +42,44 @@ void main() {
}
''');

final prices = JsonPath(r'$..price');

print('All prices in the store:');

/// The following code will print:
///
/// $['store']['book'][0]['price']: 8.95
/// $['store']['book'][1]['price']: 12.99
/// $['store']['book'][2]['price']: 8.99
/// $['store']['book'][3]['price']: 22.99
/// $['store']['bicycle']['price']: 19.95
JsonPath(r'$..price')
.filter(json)
prices
.read(json)
.map((match) => '${match.path}:\t${match.value}')
.forEach(print);

print('\n');

final bikeColor = JsonPath(r'$.store.bicycle.color');

print('A copy of the store with repainted bike:');
print(bikeColor.set(json, 'blue'));

print('\n');

print('Note, that the bike in the original store is still red:');
bikeColor
.read(json)
.map((result) => '${result.path}:\t${result.value}')
.forEach(print);

print('\n');

print('It is also possible to modify json in place '
'as long as the matching value is an object or a list:');
final someBooks = JsonPath(r'$.store.book[::2]');
someBooks.read(json).forEach((match) {
match.value['title'] = 'Banana';
});
print(json);
}
3 changes: 2 additions & 1 deletion lib/json_path.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@
library json_path;

export 'package:json_path/src/json_path.dart';
export 'package:json_path/src/result.dart';
export 'package:json_path/src/json_path_match.dart';
export 'package:json_path/src/predicate.dart';
37 changes: 0 additions & 37 deletions lib/src/ast.dart

This file was deleted.

25 changes: 25 additions & 0 deletions lib/src/ast/ast.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import 'package:json_path/src/ast/node.dart';

/// The Abstract Syntax Tree
class AST {
AST(Iterable<String> tokens) {
tokens.skipWhile((_) => _ == r'$').forEach(_processToken);
}

/// The children of the root node
Iterable<Node> get nodes => _stack.last.children;

final _stack = <Node>[Node('')];

void _processToken(String token) {
if (token == '[') {
_stack.add(Node(token));
} else if (token == ']') {
final node = _stack.removeLast();
if (node.value != '[') throw FormatException('Mismatching brackets');
_stack.last.children.add(node);
} else {
_stack.last.children.add(Node(token));
}
}
}
4 changes: 1 addition & 3 deletions lib/src/node.dart → lib/src/ast/node.dart
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
class Node {
Node(this.value, [Iterable<Node> children]) {
if (children != null) this.children.addAll(children);
}
Node(this.value);

final String value;
final children = <Node>[];
Expand Down
17 changes: 17 additions & 0 deletions lib/src/ast/tokenize.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
Iterable<String> tokenize(String expr) =>
_tokens.allMatches(expr).map((match) => match.group(0));

final _tokens = RegExp([
r'\$', // root
r'\[', // left bracket
r'\]', // right bracket
r'\.\.', // recursion
r'\.', // child
r'\*', // wildcard
r'\:', // slice
r'\,', // union
r'\?', // filter
r"'(?:[^'\\]|\\.)*'", // quoted string
r'-?\d+', // number
r'([A-Za-z_-][A-Za-z_\d-]*)', // field
].join('|'));
37 changes: 29 additions & 8 deletions lib/src/json_path.dart
Original file line number Diff line number Diff line change
@@ -1,19 +1,40 @@
import 'package:json_path/src/parser.dart';
import 'package:json_path/src/ast/ast.dart';
import 'package:json_path/src/ast/tokenize.dart';
import 'package:json_path/src/json_path_match.dart';
import 'package:json_path/src/parsing_state.dart';
import 'package:json_path/src/predicate.dart';
import 'package:json_path/src/result.dart';
import 'package:json_path/src/selector/root.dart';
import 'package:json_path/src/selector/selector.dart';

/// A JSONPath expression
class JsonPath {
/// Creates an instance from string
JsonPath(String expression, {Map<String, Predicate> filter})
: _selector = const Parser().parse(expression, filter ?? {});
/// Creates an instance from string. The [expression] is parsed once, and
/// the instance may be used many times after that.
///
/// The [filter] arguments may contain the named filters used
/// in the [expression].
///
/// Throws [FormatException] if the [expression] can not be parsed.
factory JsonPath(String expression, {Map<String, Predicate> filter}) {
if (expression.isEmpty) throw FormatException('Empty expression');
ParsingState state = Ready(RootSelector());
AST(tokenize(expression)).nodes.forEach((node) {
state = state.process(node, filter ?? {});
});
return JsonPath._(state.selector);
}

JsonPath._(this._selector);

final Selector _selector;

/// Filters the given [json] object.
/// Returns an Iterable of all elements found
Iterable<Result> filter(json) => _selector.filter([Result(json, '')]);
/// Reads the given [json] object returning an Iterable of all matches found.
Iterable<JsonPathMatch> read(json) =>
_selector.read([JsonPathMatch(json, '')]);

/// Returns a copy of [json] with all matching values replaced with [value].
dynamic set(dynamic json, dynamic value) =>
_selector.replace(json, (_) => value);

@override
String toString() => _selector.expression();
Expand Down
4 changes: 2 additions & 2 deletions lib/src/result.dart → lib/src/json_path_match.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/// A single matching result
class Result<T> {
Result(this.value, this.path);
class JsonPathMatch<T> {
JsonPathMatch(this.value, this.path);

/// The value
final T value;
Expand Down
37 changes: 0 additions & 37 deletions lib/src/parser.dart

This file was deleted.

Loading

0 comments on commit 5dd5088

Please sign in to comment.