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