chbrown / rfc6902

Complete implementation of RFC6902 in TypeScript
https://chbrown.github.io/rfc6902/
326 stars 39 forks source link

rfc6902

latest version published to npm monthly downloads from npm Travis CI build status Coverage status on Coveralls

Complete implementation of RFC6902 "JavaScript Object Notation (JSON) Patch" (including RFC6901 "JavaScript Object Notation (JSON) Pointer"), for creating and consuming application/json-patch+json documents. Also offers "diff" functionality without using Object.observe.

Demo

Simple web app using the browser-compiled version of the code.

Quickstart

Install locally

npm install --save rfc6902

Import in your script

const rfc6902 = require('rfc6902')

Calculate diff between two objects

rfc6902.createPatch({first: 'Chris'}, {first: 'Chris', last: 'Brown'})
//⇒ [ { op: 'add', path: '/last', value: 'Brown' } ]

Apply a patch to some object

const users = [{first: 'Chris', last: 'Brown', age: 20}]
rfc6902.applyPatch(users, [
  {op: 'replace', path: '/0/age', value: 21},
  {op: 'add', path: '/-', value: {first: 'Raphael', age: 37}},
])

The applyPatch function returns [null, null], indicating there were two patches, both applied successfully.

The users variable is modified in place; evaluate it to examine the end result:

users
//⇒ [ { first: 'Chris', last: 'Brown', age: 21 },
//    { first: 'Raphael', age: 37 } ]

API

In ES6 syntax:

import {applyPatch, createPatch} from 'rfc6902'

Using TypeScript annotations for clarity:

applyPatch(object: any, patch: Operation[]): Array<Error | null>

The operations in patch are applied to object in-place. Returns a list of results as long as the given patch. If all operations were successful, each item in the returned list will be null. If any of them failed, the corresponding item in the returned list will be an Error instance with descriptive .name and .message properties.

createPatch(input: any, output: any, diff?: VoidableDiff): Operation[]

Returns a list of operations (a JSON Patch) of the required operations to make input equal to output. In most cases, there is more than one way to transform an object into another. This method is more efficient than wholesale replacement, but does not always provide the optimal list of patches. It uses a simple Levenshtein-type implementation with Arrays, but it doesn't try for anything much smarter than that, so it's limited to remove, add, and replace operations.

Optional diff argument The optional `diff` argument allows the user to specify a partial function that's called before the built-in `diffAny` function. For example, to avoid recursing into instances of a custom class, say, `MyObject`: ```js function myDiff(input: any, output: any, ptr: Pointer) { if ((input instanceof MyObject || output instanceof MyObject) && input != output) { return [{op: 'replace', path: ptr.toString(), value: output}] } } const my_patch = createPatch(input, output, myDiff) ``` This will short-circuit on encountering an instance of `MyObject`, but otherwise recurse as usual.

Operation

interface Operation {
  op: 'add' | 'remove' | 'replace' | 'move' | 'copy' | 'test'
  from?: string
  path?: string
  value?: string
}

Different operations use different combinations of from / value; see JSON Patch (RFC6902) below.

Changelog

I'm not going to copy & paste my relatively descriptive commit messages into groups here; rather, these are just the changes that merited major version bumps:

4.x.x5.0.0 (2021-12-15)

3.x.x4.0.0 (2020-07-27)

2.x.x3.0.0 (2018-09-17)

Implementation details

Determinism

If you've ever implemented Levenshtein's algorithm, or played tricks with git rebase to get a reasonable sequence of commits, you'll realize that computing diffs is rarely deterministic. E.g., to transform the string abbc, you could:

  1. Delete a (⇒ b)
  2. and then append c (⇒ bc)

Or...

  1. Replace b with c (⇒ ac)
  2. and then replace a with b (⇒ bc)

Both consist of two operations, so either one is a valid solution.

Applying json-patch documents is much easier than generating them, which might explain why, when I started this project, there were more than five patch-applying RFC6902 implementations in NPM, but none for generating a patch from two distinct objects. (There was one that used Object.observe(), which only works when you're the one making the changes, and only as long as Object.observe() hasn't been deprecated, which it has.)

So when comparing your data objects, you'll want to ensure that the patches it generates meet your needs. The algorithm used by this library is not optimal, but it's more efficient than the strategy of wholesale replacing everything that's not an exact match.

Of course, this only applies to generating the patches. Applying them is deterministic and unambiguously specified by RFC6902.

JSON Pointer (RFC6901)

The RFC is a quick and easy read, but here's the gist:

E.g., consider the NPM registry:

{
  "_updated": 1417985649051,
  "flickr-with-uploads": {
    "name": "flickr-with-uploads",
    "description": "Flickr API with OAuth 1.0A and uploads",
    "repository": {
      "type": "git",
      "url": "git://github.com/chbrown/flickr-with-uploads.git"
    },
    "homepage": "https://github.com/chbrown/flickr-with-uploads",
    "keywords": [
      "flickr",
      "api",
      "backup"
    ],
    ...
  },
  ...
}
  1. /_updated: this selects the value of that key, which is just a number: 1417985649051
  2. /flickr-with-uploads: This selects the entire object:
    {
     "name": "flickr-with-uploads",
     "description": "Flickr API with OAuth 1.0A and uploads",
     "repository": {
       "type": "git",
       "url": "git://github.com/chbrown/flickr-with-uploads.git"
     },
     "homepage": "https://github.com/chbrown/flickr-with-uploads",
     "keywords": [
       "flickr",
       "api",
       "backup"
     ],
     ...
    }
  3. /flickr-with-uploads/name: this effectively applies the /name pointer to the result of the previous item, which selects the string, "flickr-with-uploads".
  4. /flickr-with-uploads/keywords/1: Array indices start at 0, so this selects the second item from the keywords array, namely, "api".

Rules:

Example

This project implements JSON Pointer functionality; e.g.:

const {Pointer} = require('rfc6902')
const repository = {
  contributors: ['chbrown', 'diachedelic', 'nathanrobinson', 'kbiedrzycki', 'stefanmaric']
}
const pointer = Pointer.fromJSON('/contributors/0')
//⇒ Pointer { tokens: [ '', 'contributors', '0' ] }
pointer.get(repository)
//⇒ 'chbrown'

JSON Patch (RFC6902)

The RFC is only 18 pages long, but here are the basics:

A JSON Patch document is a JSON document such that:

License

Copyright 2014-2021 Christopher Brown. MIT Licensed.