forked from facebook/react
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathmergeDeepInto.js
More file actions
238 lines (222 loc) · 9.2 KB
/
mergeDeepInto.js
File metadata and controls
238 lines (222 loc) · 9.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
/**
* Copyright 2013 Facebook, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* @providesModule mergeDeepInto
*/
"use strict";
var keyMirror = require('keyMirror');
var mergeHelpers = require('mergeHelpers');
var throwIf = require('throwIf');
var ArrayStrategies = mergeHelpers.ArrayStrategies;
var checkArrayStrategy = mergeHelpers.checkArrayStrategy;
var checkMergeArrayArgs = mergeHelpers.checkMergeArrayArgs;
var checkMergeLevel = mergeHelpers.checkMergeLevel;
var checkMergeObjectArgs = mergeHelpers.checkMergeObjectArgs;
var isTerminal = mergeHelpers.isTerminal;
var normalizeMergeArg = mergeHelpers.normalizeMergeArg;
var ERRORS = keyMirror({
RUN_TIME_ARRAY_MERGE_FAIL: null
});
if (__DEV__) {
ERRORS = {
RUN_TIME_ARRAY_MERGE_FAIL:
"The caller has not supplied an ArrayStrategy. This is supported as " +
"long as the data structures being merged do not contain two Arrays " +
"that must be merged together, which is exactly what has just " +
"happened. Change the call site to supply an Array merge resolver."
};
}
/**
* Every deep merge function must handle merging in each of the following cases
* at every level. We may refer to letters below in implementations. For each
* case listed, the "Result" listed describes the *value* of the result, but
* does not specify anything about the memory graph of the result. In other
* words the Results listed below will be the same for `merge` and `mergeInto`
* but `merge` may have different guarantees about mutation/cloning. In the
* table, "Object" refers to a non- Array, non-terminal object. One result is
* undefined and requires that the caller specify the resolution policy (only
* when trying to merge two arrays).
*
* Scenario Result
* --------------------------------------------------------------
* [A]: (terminal, terminal) right terminal
* [B]: (terminal, Array) right Array
* [C]: (terminal, Object) right Object
* [D]: (terminal, not present) left terminal
* [E]: (Array, terminal) right terminal
* [F]: (Array, Array) UNDEFINED - MUST SPECIFY POLICY
* [G]: (Array, Object) right Object
* [H]: (Array, not present) left Array
* [I]: (Object, terminal) right terminal
* [J]: (Object, Array) right Array
* [K]: (Object, Object) merge of left and right Objects
* [L]: (Object, not present) left Object
* [M]: (not present, terminal) right terminal
* [N]: (not present, Array) right Array
* [O]: (not present, Object) right Object
*
* All merge functions are only expected to reason about "own" properties and,
* any prototypical properties should have no effect on the result. At the first
* level of recursion in deep merges, we may choose to normalize arguments
* (convert undefined to empty objects etc.) The above chart does not describe
* the result of those top level operations which behave specially due to the
* normalization.
*/
/**
* Deep merge implementation for non-Array, non-terminal Objects.
*
* @param {!Object} one non-Array, non-terminal Object to be mutated deeply.
* @param {!Object} two non-Array, non-terminal taking precedence over `one`.
* @param {?Enum=} arrayStrategy one of `arrayStrategies`.
* @param {!number} level The level of recursion.
*/
var mergeDeepIntoObjects = function(one, two, arrayStrategy, level) {
checkMergeObjectArgs(one, two);
checkMergeLevel(level);
var twoKeys = two ? Object.keys(two) : [];
for (var i = 0; i < twoKeys.length; i++) {
var twoKey = twoKeys[i];
mergeSingleFieldDeep(one, two, twoKey, arrayStrategy, level);
}
};
/**
* Deep merge implementation for Arrays.
*
* @param {!Array} one Array to deep merge.
* @param {!Array} two Array to deep merge "into" `one`.
* @param {?Enum=} arrayStrategy one of `arrayStrategies`.
* @param {!number} level Level of recursion.
*/
var mergeDeepIntoArrays = function(one, two, arrayStrategy, level) {
checkMergeArrayArgs(one, two);
checkMergeLevel(level);
var maxLen = Math.max(one.length, two.length);
for (var i = 0; i < maxLen; i++) {
mergeSingleFieldDeep(one, two, i, arrayStrategy, level);
}
};
/**
* Given two truthy containers `one` and `two`, and a `key`, performs a deep
* merge on a logical field.
*
* @param {!Array|Object} one Container to merge into.
* @param {!Array|Object} two Container to merge from.
* @param {!string} key Key of field that should be merged.
* @param {lEnum=} arrayStrategy One of `arrayStrategies`.
* @param {!number} level Current level of recursion.
*/
var mergeSingleFieldDeep = function(one, two, key, arrayStrategy, level) {
var twoVal = two[key];
var twoValIsPresent = two.hasOwnProperty(key);
var twoValIsTerminal = twoValIsPresent && isTerminal(twoVal);
var twoValIsArray = twoValIsPresent && Array.isArray(twoVal);
var twoValIsProperObject =
twoValIsPresent && !twoValIsArray && !twoValIsArray;
var oneVal = one[key];
var oneValIsPresent = one.hasOwnProperty(key);
var oneValIsTerminal = oneValIsPresent && isTerminal(oneVal);
var oneValIsArray = oneValIsPresent && Array.isArray(oneVal);
var oneValIsProperObject =
oneValIsPresent && !oneValIsArray && !oneValIsArray;
if (oneValIsTerminal) {
if (twoValIsTerminal) { // [A]
one[key] = twoVal;
} else if (twoValIsArray) { // [B]
one[key] = [];
mergeDeepIntoArrays(one[key], twoVal, arrayStrategy, level + 1);
} else if (twoValIsProperObject) { // [C]
one[key] = {};
mergeDeepIntoObjects(one[key], twoVal, arrayStrategy, level + 1);
} else if (!twoValIsPresent) { // [D]
one[key] = oneVal;
}
} else if (oneValIsArray) {
if (twoValIsTerminal) { // [E]
one[key] = twoVal;
} else if (twoValIsArray) { // [F]
throwIf(
!ArrayStrategies[arrayStrategy],
ERRORS.RUN_TIME_ARRAY_MERGE_FAIL
);
// Else: At this point, the only other valid option is `IndexByIndex`
if (arrayStrategy === ArrayStrategies.Clobber) {
oneVal.length = 0;
}
mergeDeepIntoArrays(oneVal, twoVal, arrayStrategy, level + 1);
} else if (twoValIsProperObject) { // [G]
one[key] = {};
mergeDeepIntoObjects(one[key], twoVal, arrayStrategy, level + 1);
} else if (!twoValIsPresent) { // [H]
// Leave the left Array alone
}
} else if (oneValIsProperObject) {
if (twoValIsTerminal) { // [I]
one[key] = twoVal;
} else if (twoValIsArray) { // [J]
one[key] = [];
mergeDeepIntoArrays(one[key], twoVal, arrayStrategy, level + 1);
} else if (twoValIsProperObject) { // [K]
mergeDeepIntoObjects(oneVal, twoVal, arrayStrategy, level + 1);
} else if (!twoValIsPresent) { // [L]
// Leave the left Object alone
}
} else if (!oneValIsPresent) {
if (twoValIsTerminal) { // [M]
one[key] = twoVal;
} else if (twoValIsArray) { // [N]
one[key] = [];
mergeDeepIntoArrays(one[key], twoVal, arrayStrategy, level + 1);
} else if (twoValIsProperObject) { // [O]
one[key] = {};
mergeDeepIntoObjects(one[key], twoVal, arrayStrategy, level + 1);
} else if (!twoValIsPresent) {
// Could/should never happen
}
}
};
/**
* Provides same functionality as mergeInto, but merges by mutating the first
* argument. Will never mutate the second argument.
*
* mergeDeepInto provides two guarantees:
* 1. In the process of mutating one, will not mutate two.
* 2. Will not cause any nonTerminal memory to be shared between one and two.
* This means that no further mutations to one will effect two (unless some
* other part of your application forms shared memory between one and two).
*
* mergeDeepInto does not guarantee that it will *preserve* any shared memory
* between two and one that existed before invocation. After calling
* mergeDeepInto, there will be less (or equal) amount of shared memory between
* one and two.
*
* Will tolerate a circular structure as the first parameter, but not the
* second.
*
* Requires that first parameter be a non-Array, non-terminal `Object`, and the
* second argument either be an `Object` or `null/undefined`. Arrays may exist
* in the depths of either `Object` as long as an `arrayStrategy` is supplied.
*
* The third parameter indicates how merging two `Array`s should be performed.
*
* @param {!Object} one Object to be mutated deeply.
* @param {?Object} two Values from two take precedence over values in one.
* @param {?Enum=} arrayStrategy One of arrayStrategy
*/
var mergeDeepInto = function(one, twoParam, arrayStrategy) {
var two = normalizeMergeArg(twoParam);
checkArrayStrategy(arrayStrategy); // Will be checked twice, for now
mergeDeepIntoObjects(one, two, arrayStrategy, 0);
};
module.exports = mergeDeepInto;