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