jdiff 1.0.0 copy "jdiff: ^1.0.0" to clipboard
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;
}
0
likes
150
points
92
downloads

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

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, and bandwidth-efficient APIs.

Topics

#json #patch #diff #rfc6902 #sync

License

MIT (license)

More

Packages that depend on jdiff