1 | # rfc6902
|
2 |
|
3 | [](https://www.npmjs.com/package/rfc6902)
|
4 | [](https://www.npmjs.com/package/rfc6902)
|
5 | [](https://travis-ci.org/chbrown/rfc6902)
|
6 | [](https://coveralls.io/github/chbrown/rfc6902?branch=master)
|
7 |
|
8 | Complete 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"),
|
10 | for creating and consuming `application/json-patch+json` documents.
|
11 | Also offers "diff" functionality without using `Object.observe`.
|
12 |
|
13 |
|
14 | ## Demo
|
15 |
|
16 | Simple [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
|
24 | npm install --save rfc6902
|
25 | ```
|
26 |
|
27 | ### Import in your script
|
28 |
|
29 | ```js
|
30 | const rfc6902 = require('rfc6902')
|
31 | ```
|
32 |
|
33 | ### Calculate diff between two objects
|
34 |
|
35 | ```js
|
36 | rfc6902.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
|
43 | const users = [{first: 'Chris', last: 'Brown', age: 20}]
|
44 | rfc6902.applyPatch(users, [
|
45 | {op: 'replace', path: '/0/age', value: 21},
|
46 | {op: 'add', path: '/-', value: {first: 'Raphael', age: 37}},
|
47 | ])
|
48 | ```
|
49 | The `applyPatch` function returns `[null, null]`,
|
50 | indicating there were two patches, both applied successfully.
|
51 |
|
52 | The `users` variable is modified in place; evaluate it to examine the end result:
|
53 | ```js
|
54 | users
|
55 | //⇒ [ { first: 'Chris', last: 'Brown', age: 21 },
|
56 | // { first: 'Raphael', age: 37 } ]
|
57 | ```
|
58 |
|
59 |
|
60 | ## API
|
61 |
|
62 | In ES6 syntax:
|
63 | ```js
|
64 | import {applyPatch, createPatch} from 'rfc6902'
|
65 | ```
|
66 |
|
67 | Using [TypeScript](https://www.typescriptlang.org/) annotations for clarity:
|
68 |
|
69 | ### `applyPatch(object: any, patch: Operation[]): Array<Error | null>`
|
70 |
|
71 | The operations in `patch` are applied to `object` in-place.
|
72 | Returns a list of results as long as the given `patch`.
|
73 | If all operations were successful, each item in the returned list will be `null`.
|
74 | If any of them failed, the corresponding item in the returned list will be an Error instance
|
75 | with descriptive `.name` and `.message` properties.
|
76 |
|
77 | ### `createPatch(input: any, output: any, diff?: VoidableDiff): Operation[]`
|
78 |
|
79 | Returns a list of operations (a JSON Patch) of the required operations to make `input` equal to `output`.
|
80 | In most cases, there is more than one way to transform an object into another.
|
81 | This method is more efficient than wholesale replacement,
|
82 | but does not always provide the optimal list of patches.
|
83 | It uses a simple Levenshtein-type implementation with Arrays,
|
84 | but it doesn't try for anything much smarter than that,
|
85 | so it's limited to `remove`, `add`, and `replace` operations.
|
86 |
|
87 | <details>
|
88 | <summary>Optional <code>diff</code> argument</summary>
|
89 |
|
90 | The optional `diff` argument allows the user to specify a partial function
|
91 | that's called before the built-in `diffAny` function.
|
92 | For example, to avoid recursing into instances of a custom class, say, `MyObject`:
|
93 | ```js
|
94 | function 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 | }
|
99 | const my_patch = createPatch(input, output, myDiff)
|
100 | ```
|
101 | This will short-circuit on encountering an instance of `MyObject`, but otherwise recurse as usual.
|
102 |
|
103 | </details>
|
104 |
|
105 | ### `Operation`
|
106 |
|
107 | ```typescript
|
108 | interface Operation {
|
109 | op: 'add' | 'remove' | 'replace' | 'move' | 'copy' | 'test'
|
110 | from?: string
|
111 | path?: string
|
112 | value?: string
|
113 | }
|
114 | ```
|
115 |
|
116 | Different operations use different combinations of `from` / `value`;
|
117 | see [JSON Patch (RFC6902)](#json-patch-rfc6902) below.
|
118 |
|
119 |
|
120 | ## Changelog
|
121 |
|
122 | I'm not going to copy & paste my relatively descriptive commit messages into groups here;
|
123 | rather, 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.
|
128 | This is in violation of the spec,
|
129 | which makes no special provisions for this idiosyncrasy of the JavaScript language,
|
130 | but AFAIK there's no way to strictly comply with the spec in JavaScript.
|
131 | It would probably be more correct to throw an error in those cases,
|
132 | but 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).
|
137 | This simplifies the codebase and ensures underlying semantics do not diverge,
|
138 | but potentially does unnecessary work in computing a full diff when all we really care about is whether there is at least one difference.
|
139 | It also facilitates the user completely specifying custom diff functionality with just one `diff` function,
|
140 | as 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,
|
146 | which 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,
|
149 | but technically _was_ exported.
|
150 | If you're only importing the public API of `applyPatch`, `createPatch`, and `createTests` from `'rfc6902'`,
|
151 | nothing has changed.
|
152 |
|
153 |
|
154 | ## Implementation details
|
155 |
|
156 | ### Determinism
|
157 |
|
158 | If you've ever implemented Levenshtein's algorithm,
|
159 | or played tricks with `git rebase` to get a reasonable sequence of commits,
|
160 | you'll realize that computing diffs is rarely deterministic.
|
161 | E.g., to transform the string `ab` → `bc`, you could:
|
162 | 1. Delete `a` (⇒ `b`)
|
163 | 2. and then append `c` (⇒ `bc`)
|
164 |
|
165 | _Or..._
|
166 | 1. Replace `b` with `c` (⇒ `ac`)
|
167 | 2. and then replace `a` with `b` (⇒ `bc`)
|
168 |
|
169 | Both consist of two operations, so either one is a valid solution.
|
170 |
|
171 | Applying `json-patch` documents is much easier than generating them,
|
172 | which might explain why, when I started this project,
|
173 | there were more than five patch-applying RFC6902 implementations in NPM,
|
174 | but 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,
|
176 | and only as long as `Object.observe()` hasn't been deprecated, which it has.)
|
177 |
|
178 | So when comparing _your_ data objects, you'll want to ensure that the patches it generates meet your needs.
|
179 | The algorithm used by this library is not optimal,
|
180 | but it's more efficient than the strategy of wholesale replacing everything that's not an exact match.
|
181 |
|
182 | Of course, this only applies to generating the patches.
|
183 | Applying them is deterministic and unambiguously specified by [RFC6902](http://tools.ietf.org/html/rfc6902).
|
184 |
|
185 | ### JSON Pointer (RFC6901)
|
186 |
|
187 | The [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 |
|
197 | E.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 | ```
|
220 | 1. `/_updated`: this selects the value of that key, which is just a number: `1417985649051`
|
221 | 2. `/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 | ```
|
239 | 3. `/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"`.
|
241 | 4. `/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;
|
247 | otherwise, 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 |
|
262 | This project implements JSON Pointer functionality; e.g.:
|
263 |
|
264 | ```js
|
265 | const {Pointer} = require('rfc6902')
|
266 | const repository = {
|
267 | contributors: ['chbrown', 'diachedelic', 'nathanrobinson', 'kbiedrzycki', 'stefanmaric']
|
268 | }
|
269 | const pointer = Pointer.fromJSON('/contributors/0')
|
270 | //⇒ Pointer { tokens: [ '', 'contributors', '0' ] }
|
271 | pointer.get(repository)
|
272 | //⇒ 'chbrown'
|
273 | ```
|
274 |
|
275 | ### JSON Patch (RFC6902)
|
276 |
|
277 | The [RFC](http://tools.ietf.org/html/rfc6902) is only 18 pages long, but here are the basics:
|
278 |
|
279 | A 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 |
|
317 | Copyright 2014-2021 Christopher Brown.
|
318 | [MIT Licensed](https://chbrown.github.io/licenses/MIT/#2014-2021).
|