jdiff 1.0.0
jdiff: ^1.0.0 copied to clipboard
A pure Dart implementation of RFC 6902 JSON Patch and RFC 6901 JSON Pointer. Provides diff, patch, and apply operations for JSON-like Dart objects. Ideal for sync apps, offline-first architectures, an [...]
example/jdiff_example.dart
// ignore_for_file: avoid_print
/// jdiff — Comprehensive real-world example
///
/// Run with:
/// dart run example/jdiff_example.dart
import 'dart:convert';
import 'package:jdiff/jdiff.dart';
void main() {
_section('jdiff — RFC 6902 JSON Patch — Live Demo');
// ─────────────────────────────────────────────────────────────────────────
// Example 1: Computing and applying a diff
// ─────────────────────────────────────────────────────────────────────────
_section('1 · Diff: User profile update');
final oldProfile = <String, dynamic>{
'id': 'user_001',
'name': 'Ali Mohammed',
'email': 'ali@example.com',
'age': 25,
'address': <String, dynamic>{
'city': 'Jeddah',
'street': 'King Fahd Road',
},
'tags': <dynamic>['flutter', 'dart'],
};
final newProfile = <String, dynamic>{
'id': 'user_001',
'name': 'Ali Ahmed Mohammed', // changed
'email': 'ali@example.com', // unchanged
'age': 26, // changed
'address': <String, dynamic>{
'city': 'Riyadh', // changed city
'street': 'King Fahd Road', // unchanged
},
'tags': <dynamic>['flutter', 'dart', 'pub'], // appended
'verified': true, // new field
};
final ops = JsonPatch.diff(oldProfile, newProfile);
print(' Operations required (${ops.length} total):');
for (final op in ops) {
print(' → ${op.op.toUpperCase().padRight(8)} ${op.path}');
}
// Serialise to JSON (what you send over the wire)
final wirePayload = JsonPatch.toJsonList(ops);
print('\n Wire payload (${jsonEncode(wirePayload).length} bytes):');
print(_indent(const JsonEncoder.withIndent(' ').convert(wirePayload)));
// Apply and verify
final patched = JsonPatch.apply(oldProfile, ops);
print('\n Result equals newProfile: ${_deepEq(patched, newProfile)} ✓');
// ─────────────────────────────────────────────────────────────────────────
// Example 2: Applying a patch received from a server
// ─────────────────────────────────────────────────────────────────────────
_section('2 · Apply: Server-sent patch');
final serverPayload = <dynamic>[
<String, dynamic>{'op': 'test', 'path': '/id', 'value': 'article_42'},
<String, dynamic>{'op': 'replace', 'path': '/status', 'value': 'published'},
<String, dynamic>{'op': 'add', 'path': '/publishedAt', 'value': '2026-05-29'},
<String, dynamic>{'op': 'remove', 'path': '/draft'},
<String, dynamic>{'op': 'add', 'path': '/reviewers/-', 'value': 'editor_7'},
];
final article = <String, dynamic>{
'id': 'article_42',
'title': 'Building jdiff',
'status': 'draft',
'draft': true,
'reviewers': <dynamic>['editor_1', 'editor_3'],
};
print(' Before: ${jsonEncode(article)}');
final serverOps = JsonPatch.fromJsonList(serverPayload);
final updatedArticle = JsonPatch.apply(article, serverOps);
print(' After : ${jsonEncode(updatedArticle)}');
// ─────────────────────────────────────────────────────────────────────────
// Example 3: Optimistic locking with "test"
// ─────────────────────────────────────────────────────────────────────────
_section('3 · Optimistic locking with test');
final document = <String, dynamic>{'version': 5, 'data': 'important'};
// Scenario A: version matches — update succeeds
try {
final result = JsonPatch.apply(document, [
const TestOperation(path: '/version', value: 5),
const ReplaceOperation(path: '/data', value: 'updated safely'),
const ReplaceOperation(path: '/version', value: 6),
]);
print(' ✅ Update succeeded: ${jsonEncode(result)}');
} on JsonPatchTestFailedException catch (e) {
print(' ❌ Conflict: $e');
}
// Scenario B: stale version — update rejected
try {
JsonPatch.apply(document, [
const TestOperation(path: '/version', value: 99), // stale!
const ReplaceOperation(path: '/data', value: 'this will fail'),
]);
print(' ✅ Update succeeded (unexpected!)');
} on JsonPatchTestFailedException catch (e) {
print(' ✅ Conflict correctly detected: $e');
}
// ─────────────────────────────────────────────────────────────────────────
// Example 4: JSON Pointer navigation
// ─────────────────────────────────────────────────────────────────────────
_section('4 · JSON Pointer (RFC 6901) navigation');
final nested = <String, dynamic>{
'store': <String, dynamic>{
'name': 'Tech Books',
'inventory': <dynamic>[
<String, dynamic>{'title': 'Dart in Action', 'price': 39.99},
<String, dynamic>{'title': 'Flutter Deep Dive', 'price': 49.99},
],
},
};
final pointers = [
'/store/name',
'/store/inventory/0/title',
'/store/inventory/1/price',
];
for (final p in pointers) {
print(' $p → ${JsonPointer.parse(p).get(nested)}');
}
// Building pointers programmatically
final ptr = JsonPointer.root
.child('store')
.child('inventory')
.child('0')
.child('title');
print(' Built via .child(): $ptr → ${ptr.get(nested)}');
// ─────────────────────────────────────────────────────────────────────────
// Example 5: All six operations
// ─────────────────────────────────────────────────────────────────────────
_section('5 · All six RFC 6902 operations');
var doc = <String, dynamic>{
'a': 1,
'b': 2,
'c': 3,
'arr': <dynamic>[10, 20, 30],
};
// add
doc = JsonPatch.apply(doc, [const AddOperation(path: '/d', value: 4)])
as Map<String, dynamic>;
print(' add /d = 4 → ${jsonEncode(doc)}');
// remove
doc = JsonPatch.apply(doc, [const RemoveOperation(path: '/b')])
as Map<String, dynamic>;
print(' remove /b → ${jsonEncode(doc)}');
// replace
doc = JsonPatch.apply(doc, [const ReplaceOperation(path: '/a', value: 99)])
as Map<String, dynamic>;
print(' replace /a → ${jsonEncode(doc)}');
// move
doc = JsonPatch.apply(doc, [const MoveOperation(from: '/c', path: '/z')])
as Map<String, dynamic>;
print(' move /c→/z → ${jsonEncode(doc)}');
// copy
doc = JsonPatch.apply(doc, [const CopyOperation(from: '/z', path: '/w')])
as Map<String, dynamic>;
print(' copy /z→/w → ${jsonEncode(doc)}');
// test (verifies, no doc change)
final checked = JsonPatch.apply(doc, [const TestOperation(path: '/z', value: 3)])
as Map<String, dynamic>;
print(' test /z == 3 → ${jsonEncode(checked)} (unchanged, test passed)');
// ─────────────────────────────────────────────────────────────────────────
// Example 6: Bandwidth savings visualisation
// ─────────────────────────────────────────────────────────────────────────
_section('6 · Bandwidth savings');
final bigDoc = <String, dynamic>{
for (var i = 0; i < 50; i++) 'field_$i': 'value_${i}_original',
};
// Change only 2 out of 50 fields
final updatedBigDoc = <String, dynamic>{
...bigDoc,
'field_7': 'CHANGED_VALUE',
'field_23': 'ANOTHER_CHANGE',
};
final patch = JsonPatch.diff(bigDoc, updatedBigDoc);
final fullJson = jsonEncode(updatedBigDoc).length;
final patchJson = jsonEncode(JsonPatch.toJsonList(patch)).length;
final savings = ((1 - patchJson / fullJson) * 100).toStringAsFixed(1);
print(' Full document : $fullJson bytes');
print(' Patch payload : $patchJson bytes (${patch.length} operations)');
print(' Bandwidth saved: $savings% 🚀');
_section('Done!');
}
// ── Helpers ──────────────────────────────────────────────────────────────────
void _section(String title) {
print('\n${'─' * 60}');
print(' $title');
print('─' * 60);
}
String _indent(String s) =>
s.split('\n').map((l) => ' $l').join('\n');
bool _deepEq(Object? a, Object? b) {
if (a == b) return true;
if (a is Map && b is Map) {
if (a.length != b.length) return false;
for (final k in a.keys) {
if (!b.containsKey(k) || !_deepEq(a[k], b[k])) return false;
}
return true;
}
if (a is List && b is List) {
if (a.length != b.length) return false;
for (var i = 0; i < a.length; i++) {
if (!_deepEq(a[i], b[i])) return false;
}
return true;
}
return false;
}