codekingpro/portable-devtools
115k
1/*
2 const obj1 = {a: 3, b: 5};
3 diffApply(obj1,
4 [
5 { "op": "remove", "path": ['b'] },
6 { "op": "replace", "path": ['a'], "value": 4 },
7 { "op": "add", "path": ['c'], "value": 5 }
8 ]
9 );
10 obj1; // {a: 4, c: 5}
11
12 // using converter to apply jsPatch standard paths
13 // see http://jsonpatch.com
14 import {diff, jsonPatchPathConverter} from 'just-diff'
15 const obj2 = {a: 3, b: 5};
16 diffApply(obj2, [
17 { "op": "remove", "path": '/b' },
18 { "op": "replace", "path": '/a', "value": 4 }
19 { "op": "add", "path": '/c', "value": 5 }
20 ], jsonPatchPathConverter);
21 obj2; // {a: 4, c: 5}
22
23 // arrays
24 const obj3 = {a: 4, b: [1, 2, 3]};
25 diffApply(obj3, [
26 { "op": "replace", "path": ['a'], "value": 3 }
27 { "op": "replace", "path": ['b', 2], "value": 4 }
28 { "op": "add", "path": ['b', 3], "value": 9 }
29 ]);
30 obj3; // {a: 3, b: [1, 2, 4, 9]}
31
32 // nested paths
33 const obj4 = {a: 4, b: {c: 3}};
34 diffApply(obj4, [
35 { "op": "replace", "path": ['a'], "value": 5 }
36 { "op": "remove", "path": ['b', 'c']}
37 { "op": "add", "path": ['b', 'd'], "value": 4 }
38 ]);
39 obj4; // {a: 5, b: {d: 4}}
40*/
41
42var REMOVE = 'remove';
43var REPLACE = 'replace';
44var ADD = 'add';
45var MOVE = 'move';
46
47function diffApply(obj, diff, pathConverter) {
48 if (!obj || typeof obj != 'object') {
49 throw new Error('base object must be an object or an array');
50 }
51
52 if (!Array.isArray(diff)) {
53 throw new Error('diff must be an array');
54 }
55
56 var diffLength = diff.length;
57 for (var i = 0; i < diffLength; i++) {
58 var thisDiff = diff[i];
59 var subObject = obj;
60 var thisOp = thisDiff.op;
61
62 var thisPath = transformPath(pathConverter, thisDiff.path);
63 var thisFromPath = thisDiff.from && transformPath(pathConverter, thisDiff.from);
64 var toPath, toPathCopy, lastToProp, subToObject, valueToMove;
65
66 if (thisFromPath) {
67 // MOVE only, "fromPath" is effectively path and "path" is toPath
68 toPath = thisPath;
69 thisPath = thisFromPath;
70
71 toPathCopy = toPath.slice();
72 lastToProp = toPathCopy.pop();
73 prototypeCheck(lastToProp);
74 if (lastToProp == null) {
75 return false;
76 }
77
78 var thisToProp;
79 while (((thisToProp = toPathCopy.shift())) != null) {
80 prototypeCheck(thisToProp);
81 if (!(thisToProp in subToObject)) {
82 subToObject[thisToProp] = {};
83 }
84 subToObject = subToObject[thisToProp];
85 }
86 }
87
88 var pathCopy = thisPath.slice();
89 var lastProp = pathCopy.pop();
90 prototypeCheck(lastProp);
91 if (lastProp == null) {
92 return false;
93 }
94
95 var thisProp;
96 while (((thisProp = pathCopy.shift())) != null) {
97 prototypeCheck(thisProp);
98 if (!(thisProp in subObject)) {
99 subObject[thisProp] = {};
100 }
101 subObject = subObject[thisProp];
102 }
103 if (thisOp === REMOVE || thisOp === REPLACE || thisOp === MOVE) {
104 var path = thisOp === MOVE ? thisDiff.from : thisDiff.path;
105 if (!subObject.hasOwnProperty(lastProp)) {
106 throw new Error(['expected to find property', path, 'in object', obj].join(' '));
107 }
108 }
109 if (thisOp === REMOVE || thisOp === MOVE) {
110 if (thisOp === MOVE) {
111 valueToMove = subObject[lastProp];
112 }
113 Array.isArray(subObject) ? subObject.splice(lastProp, 1) : delete subObject[lastProp];
114 }
115 if (thisOp === REPLACE || thisOp === ADD) {
116 subObject[lastProp] = thisDiff.value;
117 }
118
119 if (thisOp === MOVE) {
120 subObject[lastToProp] = valueToMove;
121 }
122 }
123 return subObject;
124}
125
126function transformPath(pathConverter, thisPath) {
127 if(pathConverter) {
128 thisPath = pathConverter(thisPath);
129 if(!Array.isArray(thisPath)) {
130 throw new Error([
131 'pathConverter must return an array, returned:',
132 thisPath,
133 ].join(' '));
134 }
135 } else {
136 if(!Array.isArray(thisPath)) {
137 throw new Error([
138 'diff path',
139 thisPath,
140 'must be an array, consider supplying a path converter']
141 .join(' '));
142 }
143 }
144 return thisPath;
145}
146
147function jsonPatchPathConverter(stringPath) {
148 return stringPath.split('/').slice(1);
149}
150
151function prototypeCheck(prop) {
152 // coercion is intentional to catch prop values like `['__proto__']`
153 if (prop == '__proto__' || prop == 'constructor' || prop == 'prototype') {
154 throw new Error('setting of prototype values not supported');
155 }
156}
157
158export {diffApply, jsonPatchPathConverter};
159 