UNPKG

12.8 kBMarkdownView Raw
1# rfc6902
2
3[![latest version published to npm](https://badge.fury.io/js/rfc6902.svg)](https://www.npmjs.com/package/rfc6902)
4[![monthly downloads from npm](https://img.shields.io/npm/dm/rfc6902.svg?style=flat)](https://www.npmjs.com/package/rfc6902)
5[![Travis CI build status](https://travis-ci.org/chbrown/rfc6902.svg?branch=master)](https://travis-ci.org/chbrown/rfc6902)
6[![Coverage status on Coveralls](https://coveralls.io/repos/github/chbrown/rfc6902/badge.svg?branch=master)](https://coveralls.io/github/chbrown/rfc6902?branch=master)
7
8Complete implementation of [RFC6902](http://tools.ietf.org/html/rfc6902) "JavaScript Object Notation (JSON) Patch"
9(including [RFC6901](http://tools.ietf.org/html/rfc6901) "JavaScript Object Notation (JSON) Pointer"),
10for creating and consuming `application/json-patch+json` documents.
11Also offers "diff" functionality without using `Object.observe`.
12
13
14## Demo
15
16Simple [web app](https://chbrown.github.io/rfc6902/) using the browser-compiled version of the code.
17
18
19## Quickstart
20
21### Install locally
22
23```sh
24npm install --save rfc6902
25```
26
27### Import in your script
28
29```js
30const rfc6902 = require('rfc6902')
31```
32
33### Calculate diff between two objects
34
35```js
36rfc6902.createPatch({first: 'Chris'}, {first: 'Chris', last: 'Brown'})
37//⇒ [ { op: 'add', path: '/last', value: 'Brown' } ]
38```
39
40### Apply a patch to some object
41
42```js
43const users = [{first: 'Chris', last: 'Brown', age: 20}]
44rfc6902.applyPatch(users, [
45 {op: 'replace', path: '/0/age', value: 21},
46 {op: 'add', path: '/-', value: {first: 'Raphael', age: 37}},
47])
48```
49The `applyPatch` function returns `[null, null]`,
50indicating there were two patches, both applied successfully.
51
52The `users` variable is modified in place; evaluate it to examine the end result:
53```js
54users
55//⇒ [ { first: 'Chris', last: 'Brown', age: 21 },
56// { first: 'Raphael', age: 37 } ]
57```
58
59
60## API
61
62In ES6 syntax:
63```js
64import {applyPatch, createPatch} from 'rfc6902'
65```
66
67Using [TypeScript](https://www.typescriptlang.org/) annotations for clarity:
68
69### `applyPatch(object: any, patch: Operation[]): Array<Error | null>`
70
71The operations in `patch` are applied to `object` in-place.
72Returns a list of results as long as the given `patch`.
73If all operations were successful, each item in the returned list will be `null`.
74If any of them failed, the corresponding item in the returned list will be an Error instance
75with descriptive `.name` and `.message` properties.
76
77### `createPatch(input: any, output: any, diff?: VoidableDiff): Operation[]`
78
79Returns a list of operations (a JSON Patch) of the required operations to make `input` equal to `output`.
80In most cases, there is more than one way to transform an object into another.
81This method is more efficient than wholesale replacement,
82but does not always provide the optimal list of patches.
83It uses a simple Levenshtein-type implementation with Arrays,
84but it doesn't try for anything much smarter than that,
85so it's limited to `remove`, `add`, and `replace` operations.
86
87<details>
88 <summary>Optional <code>diff</code> argument</summary>
89
90The optional `diff` argument allows the user to specify a partial function
91that's called before the built-in `diffAny` function.
92For example, to avoid recursing into instances of a custom class, say, `MyObject`:
93```js
94function myDiff(input: any, output: any, ptr: Pointer) {
95 if ((input instanceof MyObject || output instanceof MyObject) && input != output) {
96 return [{op: 'replace', path: ptr.toString(), value: output}]
97 }
98}
99const my_patch = createPatch(input, output, myDiff)
100```
101This will short-circuit on encountering an instance of `MyObject`, but otherwise recurse as usual.
102
103</details>
104
105### `Operation`
106
107```typescript
108interface Operation {
109 op: 'add' | 'remove' | 'replace' | 'move' | 'copy' | 'test'
110 from?: string
111 path?: string
112 value?: string
113}
114```
115
116Different operations use different combinations of `from` / `value`;
117see [JSON Patch (RFC6902)](#json-patch-rfc6902) below.
118
119
120## Changelog
121
122I'm not going to copy & paste my relatively descriptive commit messages into groups here;
123rather, these are just the changes that merited major version bumps:
124
125### `4.x.x` → `5.0.0` (2021-12-15)
126
127* Short-circuits JSON pointer traversal over the prototype-polluting tokens `__proto__`, `constructor`, and `prototype`. I.e., `/a/__proto__/b` and `/a/b` evaluate to the same thing.
128This is in violation of the spec,
129which makes no special provisions for this idiosyncrasy of the JavaScript language,
130but AFAIK there's no way to strictly comply with the spec in JavaScript.
131It would probably be more correct to throw an error in those cases,
132but this 'solution' feels less disruptive / more in line with workarounds implemented by other libraries.
133
134### `3.x.x` → `4.0.0` (2020-07-27)
135
136* Potential performance regression due to consolidating separate `compare(a, b): boolean` and `diff(a, b): Operation[]` logic into basically defining `compare(a, b)` as `!diff(a, b).length` (i.e., `diff(a, b)` returns empty array).
137This simplifies the codebase and ensures underlying semantics do not diverge,
138but potentially does unnecessary work in computing a full diff when all we really care about is whether there is at least one difference.
139It also facilitates the user completely specifying custom diff functionality with just one `diff` function,
140as opposed to a `diff` and corresponding `compare`
141(and avoids the headache of having to propagate both of those around internally).
142
143### `2.x.x` → `3.0.0` (2018-09-17)
144
145* Corrects improper behavior in a few buggy edge cases,
146which might conceivably break consumers relying on incorrect behavior.
147(Tbh [that applies to most bugfixes](https://xkcd.com/1172/) but I felt there were enough to add up to incrementing the major version.)
148* Also moves around some of the internal API that was not intended to be used externally,
149but technically _was_ exported.
150If you're only importing the public API of `applyPatch`, `createPatch`, and `createTests` from `'rfc6902'`,
151nothing has changed.
152
153
154## Implementation details
155
156### Determinism
157
158If you've ever implemented Levenshtein's algorithm,
159or played tricks with `git rebase` to get a reasonable sequence of commits,
160you'll realize that computing diffs is rarely deterministic.
161E.g., to transform the string `ab``bc`, you could:
1621. Delete `a` (⇒ `b`)
1632. and then append `c` (⇒ `bc`)
164
165_Or..._
1661. Replace `b` with `c` (⇒ `ac`)
1672. and then replace `a` with `b` (⇒ `bc`)
168
169Both consist of two operations, so either one is a valid solution.
170
171Applying `json-patch` documents is much easier than generating them,
172which might explain why, when I started this project,
173there were more than five patch-applying RFC6902 implementations in NPM,
174but none for generating a patch from two distinct objects.
175(There was one that used `Object.observe()`, which only works when you're the one making the changes,
176and only as long as `Object.observe()` hasn't been deprecated, which it has.)
177
178So when comparing _your_ data objects, you'll want to ensure that the patches it generates meet your needs.
179The algorithm used by this library is not optimal,
180but it's more efficient than the strategy of wholesale replacing everything that's not an exact match.
181
182Of course, this only applies to generating the patches.
183Applying them is deterministic and unambiguously specified by [RFC6902](http://tools.ietf.org/html/rfc6902).
184
185### JSON Pointer (RFC6901)
186
187The [RFC](http://tools.ietf.org/html/rfc6901) is a quick and easy read, but here's the gist:
188
189* JSON Pointer is a system for pointing to some fragment of a JSON document.
190* A pointer is a string that is composed of zero or more <code>/<i>reference-token</i></code> parts.
191 - When there are zero (the empty string), the pointer indicates the entire JSON document.
192 - Otherwise, the parts are read from left to right, each one selecting part of the current document,
193 and presenting only that fragment of the document to the next part.
194* The <code><i>reference-token</i></code> bits are usually Object keys,
195 but may also be (decimal) numerals, to indicate array indices.
196
197E.g., consider the NPM registry:
198
199```js
200{
201 "_updated": 1417985649051,
202 "flickr-with-uploads": {
203 "name": "flickr-with-uploads",
204 "description": "Flickr API with OAuth 1.0A and uploads",
205 "repository": {
206 "type": "git",
207 "url": "git://github.com/chbrown/flickr-with-uploads.git"
208 },
209 "homepage": "https://github.com/chbrown/flickr-with-uploads",
210 "keywords": [
211 "flickr",
212 "api",
213 "backup"
214 ],
215 ...
216 },
217 ...
218}
219```
2201. `/_updated`: this selects the value of that key, which is just a number: `1417985649051`
2212. `/flickr-with-uploads`: This selects the entire object:
222 ```js
223 {
224 "name": "flickr-with-uploads",
225 "description": "Flickr API with OAuth 1.0A and uploads",
226 "repository": {
227 "type": "git",
228 "url": "git://github.com/chbrown/flickr-with-uploads.git"
229 },
230 "homepage": "https://github.com/chbrown/flickr-with-uploads",
231 "keywords": [
232 "flickr",
233 "api",
234 "backup"
235 ],
236 ...
237 }
238 ```
2393. `/flickr-with-uploads/name`: this effectively applies the `/name` pointer to the result of the previous item,
240 which selects the string, `"flickr-with-uploads"`.
2414. `/flickr-with-uploads/keywords/1`: Array indices start at 0,
242 so this selects the second item from the `keywords` array, namely, `"api"`.
243
244#### Rules:
245
246* A pointer, if it is not empty, must always start with a slash;
247otherwise, it is an "Invalid pointer syntax" error.
248* If a key within the JSON document contains a forward slash character
249 (which is totally valid JSON, but not very nice),
250 the `/` in the desired key should be replaced by the escape sequence, `~1`.
251* If a key within the JSON document contains a tilde (again valid JSON, but not very common),
252 the `~` should be replaced by the other escape sequence, `~0`.
253 This allows keys containing the literal string `~1` (which is especially cruel)
254 to be referenced by a JSON pointer (e.g., `/~01` should return `true` when applied to the object `{"~1":true}`).
255* All double quotation marks, reverse slashes,
256 and control characters _must_ escaped, since a JSON Pointer is a JSON string.
257* A pointer that refers to a non-existent value counts as an error, too.
258 But not necessarily as fatal as a syntax error.
259
260#### Example
261
262This project implements JSON Pointer functionality; e.g.:
263
264```js
265const {Pointer} = require('rfc6902')
266const repository = {
267 contributors: ['chbrown', 'diachedelic', 'nathanrobinson', 'kbiedrzycki', 'stefanmaric']
268}
269const pointer = Pointer.fromJSON('/contributors/0')
270//⇒ Pointer { tokens: [ '', 'contributors', '0' ] }
271pointer.get(repository)
272//⇒ 'chbrown'
273```
274
275### JSON Patch (RFC6902)
276
277The [RFC](http://tools.ietf.org/html/rfc6902) is only 18 pages long, but here are the basics:
278
279A JSON Patch document is a JSON document such that:
280
281* The MIME Type is `application/json-patch+json`
282* The file extension is `.json-patch`
283* It is an array of patch objects, potentially empty.
284* Each patch object has a key, `op`, with one of the following six values,
285 and an operator-specific set of other keys.
286 - **`add`**: Insert the given `value` at `path`. Or replace it, if it already exists.
287 If the parent of the intended target does not exist, produce an error.
288 If the final reference-token of `path` is "`-`", and the parent is an array, append `value` to it.
289 + `path`: JSON Pointer
290 + `value`: JSON object
291 - **`remove`**: Remove the value at `path`. Produces an error if it does not exist.
292 If `path` refers to an element within an array,
293 splice it out so that subsequent elements fill in the gap (decrementing the length of the array).
294 + `path`: JSON Pointer
295 - **`replace`**: Replace the current value at `path` with `value`.
296 It's exactly the same as performing a `remove` operation and then an `add` operation on the same path,
297 since there _must_ be a pre-existing value.
298 + `path`: JSON Pointer
299 + `value`: JSON object
300 - **`move`**: Remove the value at `from`, and set `path` to that value.
301 There _must_ be a value at `from`, but not necessarily at `path`;
302 it's the same as performing a `remove` operation, and then an `add` operation, but on different paths.
303 + `from`: JSON Pointer
304 + `path`: JSON Pointer
305 - **`copy`**: Get the value at `from` and set `path` to that value.
306 Same as `move`, but doesn't remove the original value.
307 + `from`: JSON Pointer
308 + `path`: JSON Pointer
309 - **`test`**: Check that the value at `path` is equal to `value`.
310 If it is not, the entire patch is considered to be a failure.
311 + `path`: JSON Pointer
312 + `value`: JSON object
313
314
315## License
316
317Copyright 2014-2021 Christopher Brown.
318[MIT Licensed](https://chbrown.github.io/licenses/MIT/#2014-2021).