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