Skip to content

Commit

Permalink
JSON Pointer
Browse files Browse the repository at this point in the history
  • Loading branch information
f3ath committed Jan 11, 2021
1 parent 79970d0 commit 04ada88
Show file tree
Hide file tree
Showing 29 changed files with 446 additions and 88 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
- `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.
- JsonPointer implementation

### Changed
- Named filters argument renamed from `filter` to `filters`
Expand Down
21 changes: 13 additions & 8 deletions example/main.dart → example/example.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import 'dart:convert';
import 'package:json_path/json_path.dart';

void main() {
final json = jsonDecode('''
final document = jsonDecode('''
{
"store": {
"book": [
Expand Down Expand Up @@ -48,13 +48,18 @@ void main() {

/// 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
/// /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
prices
.read(json)
.map((match) => '${match.path}:\t${match.value}')
.read(document)
.map((match) => '${match.pointer}:\t${match.value}')
.forEach(print);

/// Modifying all prices in-place:
prices.read(document).map((match) => match.pointer).forEach((pointer) {
pointer.transform(document, (value) => value - 1);
});
}
9 changes: 4 additions & 5 deletions lib/json_path.dart
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
/// JSONPath for Dart
library json_path;

export 'package:json_path/json_pointer.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';
export 'package:json_path/src/matching_context.dart';
export 'package:json_path/src/path/filter_not_found.dart';
export 'package:json_path/src/path/json_path.dart';
export 'package:json_path/src/path/json_path_match.dart';
export 'package:json_path/src/path/matching_context.dart';
21 changes: 4 additions & 17 deletions lib/json_pointer.dart
Original file line number Diff line number Diff line change
@@ -1,18 +1,5 @@
/// JSON Pointer (RFC 6901).
class JsonPointer {
/// Creates a pointer to the root element
const JsonPointer() : value = '';
library json_pointer;

JsonPointer._(this.value);

/// The string value of the pointer
final String value;

/// Returns a new instance of [JsonPointer]
/// with the [segment] appended at the end.
JsonPointer append(String segment) => JsonPointer._(
value + '/' + segment.replaceAll('~', '~0').replaceAll('/', '~1'));

@override
String toString() => value;
}
export 'package:json_path/src/pointer/json_pointer.dart';
export 'package:json_path/src/pointer/reference.dart';
export 'package:json_path/src/pointer/invalid_route.dart';
7 changes: 0 additions & 7 deletions lib/src/build_parser.dart

This file was deleted.

4 changes: 2 additions & 2 deletions lib/src/any_match.dart → lib/src/path/any_match.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import 'package:json_path/json_pointer.dart';
import 'package:json_path/src/json_path_match.dart';
import 'package:json_path/src/matching_context.dart';
import 'package:json_path/src/path/json_path_match.dart';
import 'package:json_path/src/path/matching_context.dart';

class AnyMatch<T> implements JsonPathMatch<T> {
const AnyMatch(
Expand Down
7 changes: 7 additions & 0 deletions lib/src/path/build_parser.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import 'package:json_path/src/path/grammar.dart';
import 'package:json_path/src/path/selector/selector.dart';
import 'package:petitparser/core.dart';

Parser<Selector> buildParser() {
return jsonPath;
}
File renamed without changes.
18 changes: 9 additions & 9 deletions lib/src/grammar.dart → lib/src/path/grammar.dart
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import 'package:json_path/src/selector/array_index.dart';
import 'package:json_path/src/selector/array_slice.dart';
import 'package:json_path/src/selector/field.dart';
import 'package:json_path/src/selector/named_filter.dart';
import 'package:json_path/src/selector/recursion.dart';
import 'package:json_path/src/selector/selector.dart';
import 'package:json_path/src/selector/sequence.dart';
import 'package:json_path/src/selector/union.dart';
import 'package:json_path/src/selector/wildcard.dart';
import 'package:json_path/src/path/selector/array_index.dart';
import 'package:json_path/src/path/selector/array_slice.dart';
import 'package:json_path/src/path/selector/field.dart';
import 'package:json_path/src/path/selector/named_filter.dart';
import 'package:json_path/src/path/selector/recursion.dart';
import 'package:json_path/src/path/selector/selector.dart';
import 'package:json_path/src/path/selector/sequence.dart';
import 'package:json_path/src/path/selector/union.dart';
import 'package:json_path/src/path/selector/wildcard.dart';
import 'package:petitparser/petitparser.dart';

final minus = char('-');
Expand Down
8 changes: 4 additions & 4 deletions lib/src/json_path.dart → lib/src/path/json_path.dart
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import 'package:json_path/src/grammar.dart' as grammar;
import 'package:json_path/src/json_path_match.dart';
import 'package:json_path/src/match_factory.dart';
import 'package:json_path/src/selector/selector.dart';
import 'package:json_path/src/path/grammar.dart' as grammar;
import 'package:json_path/src/path/json_path_match.dart';
import 'package:json_path/src/path/match_factory.dart';
import 'package:json_path/src/path/selector/selector.dart';

/// A JSONPath expression
class JsonPath {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import 'package:json_path/json_pointer.dart';
import 'package:json_path/src/matching_context.dart';
import 'package:json_path/src/path/matching_context.dart';

/// A named filter function
typedef CallbackFilter = bool Function(JsonPathMatch match);
Expand Down
10 changes: 5 additions & 5 deletions lib/src/match_factory.dart → lib/src/path/match_factory.dart
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
import 'package:json_path/json_pointer.dart';
import 'package:json_path/src/any_match.dart';
import 'package:json_path/src/json_path_match.dart';
import 'package:json_path/src/matching_context.dart';
import 'package:json_path/src/quote.dart';
import 'package:json_path/src/path/any_match.dart';
import 'package:json_path/src/path/json_path_match.dart';
import 'package:json_path/src/path/matching_context.dart';
import 'package:json_path/src/path/quote.dart';

/// Creates a match for the root element
JsonPathMatch rootMatch(
dynamic value, String expression, Map<String, CallbackFilter> filter) =>
_newMatch(
value: value,
path: r'$',
pointer: const JsonPointer(),
pointer: JsonPointer(),
context: MatchingContext(expression, filter),
parent: null);

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import 'package:json_path/src/json_path_match.dart';
import 'package:json_path/src/path/json_path_match.dart';

class MatchingContext {
const MatchingContext(this.expression, this.filters);
Expand Down
File renamed without changes.
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import 'package:json_path/src/json_path_match.dart';
import 'package:json_path/src/match_factory.dart';
import 'package:json_path/src/selector/selector.dart';
import 'package:json_path/src/path/json_path_match.dart';
import 'package:json_path/src/path/match_factory.dart';
import 'package:json_path/src/path/selector/selector.dart';

class ArrayIndex implements Selector {
ArrayIndex(this.index);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import 'dart:math';

import 'package:json_path/src/json_path_match.dart';
import 'package:json_path/src/match_factory.dart';
import 'package:json_path/src/selector/selector.dart';
import 'package:json_path/src/path/json_path_match.dart';
import 'package:json_path/src/path/match_factory.dart';
import 'package:json_path/src/path/selector/selector.dart';

class ArraySlice implements Selector {
ArraySlice({this.start, this.stop, int? step}) : step = step ?? 1;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import 'package:json_path/src/json_path_match.dart';
import 'package:json_path/src/match_factory.dart';
import 'package:json_path/src/selector/selector.dart';
import 'package:json_path/src/path/json_path_match.dart';
import 'package:json_path/src/path/match_factory.dart';
import 'package:json_path/src/path/selector/selector.dart';

class Field implements Selector {
Field(this.name);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import 'package:json_path/src/filter_not_found.dart';
import 'package:json_path/src/json_path_match.dart';
import 'package:json_path/src/selector/selector.dart';
import 'package:json_path/src/path/filter_not_found.dart';
import 'package:json_path/src/path/json_path_match.dart';
import 'package:json_path/src/path/selector/selector.dart';

class NamedFilter implements Selector {
NamedFilter(this.name);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import 'package:json_path/src/json_path_match.dart';
import 'package:json_path/src/selector/selector.dart';
import 'package:json_path/src/selector/wildcard.dart';
import 'package:json_path/src/path/json_path_match.dart';
import 'package:json_path/src/path/selector/selector.dart';
import 'package:json_path/src/path/selector/wildcard.dart';

class Recursion implements Selector {
const Recursion();
Expand Down
6 changes: 6 additions & 0 deletions lib/src/path/selector/selector.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import 'package:json_path/src/path/json_path_match.dart';

abstract class Selector {
/// Applies this filter to the [match]
Iterable<JsonPathMatch> read(JsonPathMatch match);
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import 'package:json_path/src/json_path_match.dart';
import 'package:json_path/src/selector/selector.dart';
import 'package:json_path/src/path/json_path_match.dart';
import 'package:json_path/src/path/selector/selector.dart';

typedef Filter = Iterable<JsonPathMatch> Function(
Iterable<JsonPathMatch> matches);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import 'package:json_path/src/json_path_match.dart';
import 'package:json_path/src/selector/selector.dart';
import 'package:json_path/src/path/json_path_match.dart';
import 'package:json_path/src/path/selector/selector.dart';

class Union implements Selector {
const Union(this._elements);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import 'package:json_path/src/json_path_match.dart';
import 'package:json_path/src/match_factory.dart';
import 'package:json_path/src/selector/selector.dart';
import 'package:json_path/src/path/json_path_match.dart';
import 'package:json_path/src/path/match_factory.dart';
import 'package:json_path/src/path/selector/selector.dart';

class Wildcard implements Selector {
const Wildcard();
Expand Down
9 changes: 9 additions & 0 deletions lib/src/pointer/invalid_route.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
/// Thrown when the referenced value is not found in the document.
class InvalidRoute implements Exception {
const InvalidRoute(this.pointer);

final String pointer;

@override
String toString() => 'No value is referenced by $pointer';
}
104 changes: 104 additions & 0 deletions lib/src/pointer/json_pointer.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import 'package:json_path/src/pointer/invalid_route.dart';
import 'package:json_path/src/pointer/reference.dart';

/// A JSON Pointer [RFC 6901](https://tools.ietf.org/html/rfc6901).
class JsonPointer {
const JsonPointer();

/// An empty JSON Pointer
static const empty = JsonPointer();

/// Parses a new JSON Pointer from a string expression.
/// This method returns a non-writable [JsonPointer] for an empty expression
/// and [WritableJsonPointer] for a non-empty expression.
static JsonPointer parse(String expression) {
if (expression.isEmpty) return empty;
return parseWritable(expression);
}

/// Parses a new writable JSON Pointer from a non-empty string expression.
/// Throws a [FormatException] if the expression has invalid format.
static WritableJsonPointer parseWritable(String expression) {
final errors = _errors(expression);
if (errors.isNotEmpty) throw FormatException(errors.join(' '));
return buildWritable(expression.split('/').skip(1).map(_unescape));
}

/// Builds a new writable JSON Pointer from a non-empty iterable of segments.
/// Throws a [ArgumentError] if the iterable is empty.
static WritableJsonPointer buildWritable(Iterable<String> segments) {
if (segments.isEmpty) throw ArgumentError('Empty segments');
return segments.skip(1).fold(empty.append(segments.first),
(previousValue, element) => previousValue.append(element));
}

/// Returns errors found in the [expression].
/// The expression is valid if no errors are returned.
static Iterable<String> _errors(String expression) sync* {
if (!expression.startsWith('/')) {
yield 'Expression MUST start with "/".';
}
if (_danglingTilda.hasMatch(expression)) {
yield 'Tilda("~") MUST be followed by "0" or "1".';
}
}

static String _unescape(String s) =>
s.replaceAll('~1', '/').replaceAll('~0', '~');

static final _danglingTilda = RegExp(r'~[^01]|~$');

/// Reads the referenced value from the [document].
/// If no value is referenced, tries to return the result of [orElse].
/// Otherwise throws [InvalidRoute].
dynamic read(document, {dynamic Function()? orElse}) => document;

/// Returns a new writable JSON Pointer by appending a new [reference] at the end.
WritableJsonPointer append(String reference) =>
WritableJsonPointer(JsonPointerReference.parse(reference), parent: this);

@override
String toString() => '';
}

/// A writable JSON Pointer.
class WritableJsonPointer extends JsonPointer {
/// Creates a new instance of [WritableJsonPointer] from the [reference].
/// If [parent] is passed, the [reference] will be appended to it.
const WritableJsonPointer(this.reference,
{this.parent = const JsonPointer()});

/// The rightmost reference in the pointer
final JsonPointerReference reference;

/// The parent pointer
final JsonPointer parent;

@override
dynamic read(dynamic document, {dynamic Function()? orElse}) {
try {
return reference.read(parent.read(document));
} on InvalidRoute catch (e) {
if (orElse != null) return orElse();
throw _wrapped(e);
}
}

/// Replaces the referenced value in the [document] with a [newValue].
/// When a non-existing [Map] (JSON Object) key is referred, it will be added to the map.
/// When a new index in a [List] (JSON Array) is referred, it will be appended to the list.
/// Otherwise throws [InvalidRoute].
void write(dynamic document, dynamic newValue) {
try {
reference.write(parent.read(document), newValue);
} on InvalidRoute catch (e) {
throw _wrapped(e);
}
}

@override
String toString() => '$parent$reference';

InvalidRoute _wrapped(InvalidRoute e) => InvalidRoute(
'No value is referenced by $this. Failed reference: ${e.pointer}');
}
Loading

0 comments on commit 04ada88

Please sign in to comment.