LegacyImmutableObject module
This copies the old implementation of ImmutableObject into LegacyImmutableObject.
Kunal Mehta committed
Mar 4, 2014 at 18:10 UTC
4ad320dd134215353f20cb5f15afd1b3331e2e26
2 files changed
+491
src/utils/LegacyImmutableObject.js
new
+193
@@ -0,0 +1,193 @@
1
+/**
2
+ * Copyright 2013-2014 Facebook, Inc.
3
+ *
4
+ * Licensed under the Apache License, Version 2.0 (the "License");
5
+ * you may not use this file except in compliance with the License.
6
+ * You may obtain a copy of the License at
7
+ *
8
+ * http://www.apache.org/licenses/LICENSE-2.0
9
+ *
10
+ * Unless required by applicable law or agreed to in writing, software
11
+ * distributed under the License is distributed on an "AS IS" BASIS,
12
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ * See the License for the specific language governing permissions and
14
+ * limitations under the License.
15
+ *
16
+ * @providesModule LegacyImmutableObject
17
+ * @typechecks
18
+ */
19
+
20
+"use strict";
21
+
22
+var invariant = require('invariant');
23
+var isNode = require('isNode');
24
+var merge = require('merge');
25
+var mergeInto = require('mergeInto');
26
+var mergeHelpers = require('mergeHelpers');
27
+
28
+var checkMergeObjectArgs = mergeHelpers.checkMergeObjectArgs;
29
+var isTerminal = mergeHelpers.isTerminal;
30
+
31
+/**
32
+ * Wrapper around JavaScript objects that provide a guarantee of immutability at
33
+ * developer time when strict mode is used. The extra computations required to
34
+ * enforce immutability is stripped out in production for performance reasons.
35
+ */
36
+var LegacyImmutableObject;
37
+
38
+function assertLegacyImmutableObject(immutableObject) {
39
+ invariant(
40
+ immutableObject instanceof LegacyImmutableObject,
41
+ 'LegacyImmutableObject: Attempted to set fields on an object that is not ' +
42
+ 'an instance of LegacyImmutableObject.'
43
+ );
44
+}
45
+
46
+if (__DEV__) {
47
+ /**
48
+ * Constructs an instance of `LegacyImmutableObject`.
49
+ *
50
+ * @param {?object} initialProperties The initial set of properties.
51
+ * @constructor
52
+ */
53
+ LegacyImmutableObject = function LegacyImmutableObject(initialProperties) {
54
+ mergeInto(this, initialProperties);
55
+ deepFreeze(this);
56
+ };
57
+
58
+ /**
59
+ * Checks if an object should be deep frozen. Instances of
60
+ * `LegacyImmutableObject` are assumed to have already been deep frozen.
61
+ *
62
+ * @param {*} object The object to check.
63
+ * @return {boolean} Whether or not deep freeze is needed.
64
+ */
65
+ var shouldRecurseFreeze = function(object) {
66
+ return (
67
+ typeof object === 'object' &&
68
+ !(object instanceof LegacyImmutableObject) &&
69
+ object !== null
70
+ );
71
+ };
72
+
73
+ /**
74
+ * Freezes the supplied object deeply.
75
+ *
76
+ * @param {*} object The object to freeze.
77
+ */
78
+ var deepFreeze = function(object) {
79
+ if (isNode(object)) {
80
+ return; // Don't try to freeze DOM nodes.
81
+ }
82
+ Object.freeze(object); // First freeze the object.
83
+ for (var prop in object) {
84
+ var field = object[prop];
85
+ if (object.hasOwnProperty(prop) && shouldRecurseFreeze(field)) {
86
+ deepFreeze(field);
87
+ }
88
+ }
89
+ };
90
+
91
+ /**
92
+ * Returns a new LegacyImmutableObject that is identical to the supplied
93
+ * object but with the supplied changes, `put`.
94
+ *
95
+ * @param {LegacyImmutableObject} immutableObject Starting object.
96
+ * @param {?object} put Fields to merge into the object.
97
+ * @return {LegacyImmutableObject} The result of merging in `put` fields.
98
+ */
99
+ LegacyImmutableObject.set = function(immutableObject, put) {
100
+ assertLegacyImmutableObject(immutableObject);
101
+ var totalNewFields = merge(immutableObject, put);
102
+ return new LegacyImmutableObject(totalNewFields);
103
+ };
104
+
105
+} else {
106
+ /**
107
+ * Constructs an instance of `LegacyImmutableObject`.
108
+ *
109
+ * @param {?object} initialProperties The initial set of properties.
110
+ * @constructor
111
+ */
112
+ LegacyImmutableObject = function LegacyImmutableObject(initialProperties) {
113
+ mergeInto(this, initialProperties);
114
+ };
115
+
116
+ /**
117
+ * Returns a new LegacyImmutableObject that is identical to the supplied
118
+ * object but with the supplied changes, `put`.
119
+ *
120
+ * @param {LegacyImmutableObject} immutableObject Starting object.
121
+ * @param {?object} put Fields to merge into the object.
122
+ * @return {LegacyImmutableObject} The result of merging in `put` fields.
123
+ */
124
+ LegacyImmutableObject.set = function(immutableObject, put) {
125
+ assertLegacyImmutableObject(immutableObject);
126
+ var newMap = new LegacyImmutableObject(immutableObject);
127
+ mergeInto(newMap, put);
128
+ return newMap;
129
+ };
130
+}
131
+
132
+/**
133
+ * Sugar for:
134
+ * `LegacyImmutableObject.set(LegacyImmutableObject, {fieldName: putField})`.
135
+ *
136
+ * @param {LegacyImmutableObject} immutableObject Object on which to set field.
137
+ * @param {string} fieldName Name of the field to set.
138
+ * @param {*} putField Value of the field to set.
139
+ * @return {LegacyImmutableObject} [description]
140
+ */
141
+LegacyImmutableObject.setField = function(immutableObject, fieldName, putField) {
142
+ var put = {};
143
+ put[fieldName] = putField;
144
+ return LegacyImmutableObject.set(immutableObject, put);
145
+};
146
+
147
+/**
148
+ * Returns a new LegacyImmutableObject that is identical to the supplied object
149
+ * but with the supplied changes recursively applied.
150
+ *
151
+ * @param {LegacyImmutableObject} immutableObject Object on which to set fields.
152
+ * @param {object} put Fields to merge into the object.
153
+ * @return {LegacyImmutableObject} The result of merging in `put` fields.
154
+ */
155
+LegacyImmutableObject.setDeep = function(immutableObject, put) {
156
+ assertLegacyImmutableObject(immutableObject);
157
+ return _setDeep(immutableObject, put);
158
+};
159
+
160
+function _setDeep(object, put) {
161
+ checkMergeObjectArgs(object, put);
162
+ var totalNewFields = {};
163
+
164
+ // To maintain the order of the keys, copy the base object's entries first.
165
+ var keys = Object.keys(object);
166
+ for (var ii = 0; ii < keys.length; ii++) {
167
+ var key = keys[ii];
168
+ if (!put.hasOwnProperty(key)) {
169
+ totalNewFields[key] = object[key];
170
+ } else if (isTerminal(object[key]) || isTerminal(put[key])) {
171
+ totalNewFields[key] = put[key];
172
+ } else {
173
+ totalNewFields[key] = _setDeep(object[key], put[key]);
174
+ }
175
+ }
176
+
177
+ // Apply any new keys that the base object didn't have.
178
+ var newKeys = Object.keys(put);
179
+ for (ii = 0; ii < newKeys.length; ii++) {
180
+ var newKey = newKeys[ii];
181
+ if (object.hasOwnProperty(newKey)) {
182
+ continue;
183
+ }
184
+ totalNewFields[newKey] = put[newKey];
185
+ }
186
+
187
+ return (object instanceof LegacyImmutableObject ||
188
+ put instanceof LegacyImmutableObject) ?
189
+ new LegacyImmutableObject(totalNewFields) :
190
+ totalNewFields;
191
+}
192
+
193
+module.exports = LegacyImmutableObject;
src/utils/__tests__/LegacyImmutableObject-test.js
new
+298
@@ -0,0 +1,298 @@
1
+/**
2
+ * Copyright 2013-2014 Facebook, Inc.
3
+ *
4
+ * Licensed under the Apache License, Version 2.0 (the "License");
5
+ * you may not use this file except in compliance with the License.
6
+ * You may obtain a copy of the License at
7
+ *
8
+ * http://www.apache.org/licenses/LICENSE-2.0
9
+ *
10
+ * Unless required by applicable law or agreed to in writing, software
11
+ * distributed under the License is distributed on an "AS IS" BASIS,
12
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ * See the License for the specific language governing permissions and
14
+ * limitations under the License.
15
+ *
16
+ * @emails react-core
17
+ */
18
+
19
+"use strict";
20
+
21
+require('mock-modules')
22
+ .dontMock('LegacyImmutableObject');
23
+
24
+var LegacyImmutableObject;
25
+
26
+/**
27
+ * To perform performance testing of using `LegacyImmutableObject` vs. not using
28
+ * `LegacyImmutableObject`, such testing must be done with __DEV__ set to false.
29
+ */
30
+describe('LegacyImmutableObject', function() {
31
+ var message;
32
+ beforeEach(function() {
33
+ require('mock-modules').dumpCache();
34
+ LegacyImmutableObject = require('LegacyImmutableObject');
35
+ this.addMatchers({
36
+ /**
37
+ * Equivalent with respect to serialization. Must stringify because
38
+ * constructors are different and other comparison methods will not
39
+ * consider them structurally equal. Probably not useful for use outside
40
+ * of this test module.
41
+ */
42
+ toBeSeriallyEqualTo: function(expected) {
43
+ var actual = this.actual;
44
+ var notText = this.isNot ? " not" : "";
45
+ this.message = function () {
46
+ return "Expected " + JSON.stringify(actual) + notText +
47
+ " to be serially equal to " + JSON.stringify(expected);
48
+ };
49
+
50
+ return JSON.stringify(actual) === JSON.stringify(expected);
51
+ }
52
+ });
53
+ });
54
+
55
+ /**
56
+ * We are in __DEV__ by default.
57
+ */
58
+ var testDev = function(message, testFunc) {
59
+ it(message, testFunc);
60
+ };
61
+
62
+ var testProd = function(message, testFunc) {
63
+ // Temporarily enter production mode
64
+ window.__DEV__ = false;
65
+ it(message, testFunc);
66
+ window.__DEV__ = true;
67
+ };
68
+
69
+ var testDevAndProd = function(message, testFunc) {
70
+ testDev(message + ':DEV', testFunc);
71
+ testProd(message + ':PROD', testFunc);
72
+ };
73
+
74
+ testDev('should be running in DEV', function() {
75
+ expect(window.__DEV__).toBe(true);
76
+ });
77
+
78
+ testDev('should require initial map to be an object', function() {
79
+ expect(function() {
80
+ new LegacyImmutableObject([1,2,3]);
81
+ }).toThrow();
82
+
83
+ expect(function() {
84
+ new LegacyImmutableObject('asdf');
85
+ }).toThrow();
86
+
87
+ expect(function() {
88
+ new LegacyImmutableObject({oldField: 'asdf', fieldTwo: null});
89
+ }).not.toThrow();
90
+ });
91
+
92
+ testDev('should not exceed maximum call stack size with nodes', function() {
93
+ var node = document.createElement('div');
94
+ var object = new LegacyImmutableObject({node: node});
95
+ expect(object.node).toBe(node);
96
+ });
97
+
98
+ testDevAndProd('should not throw when not mutating directly', function() {
99
+ var io = new LegacyImmutableObject({oldField: 'asdf'});
100
+ expect(function() {
101
+ LegacyImmutableObject.set(io, {newField: null}); // not a mutation!
102
+ }).not.toThrow();
103
+ });
104
+
105
+ testDev('should prevent shallow field addition when strict', function() {
106
+ expect(function() {
107
+ var io = new LegacyImmutableObject({oldField: 'asdf'});
108
+ io.newField = 'this will not work';
109
+ }).toThrow();
110
+ });
111
+
112
+ testDev('should prevent shallow field mutation when strict', function() {
113
+ expect(function() {
114
+ var io = new LegacyImmutableObject({oldField: 'asdf'});
115
+ io.oldField = 'this will not work!';
116
+ }).toThrow();
117
+ });
118
+
119
+ testDev('should prevent deep field addition when strict', function() {
120
+ expect(function() {
121
+ var io =
122
+ new LegacyImmutableObject(
123
+ {shallowField: {deepField: {oldField: null}}}
124
+ );
125
+ io.shallowField.deepField.oldField = 'this will not work!';
126
+ }).toThrow();
127
+ });
128
+
129
+ testDev('should prevent deep field mutation when strict', function() {
130
+ expect(function() {
131
+ var io =
132
+ new LegacyImmutableObject(
133
+ {shallowField: {deepField: {oldField: null}}}
134
+ );
135
+ io.shallowField.deepField.newField = 'this will not work!';
136
+ }).toThrow();
137
+ });
138
+
139
+ testDevAndProd(
140
+ 'should create object with same structure when set with {}',
141
+ function() {
142
+ var beforeIO =
143
+ new LegacyImmutableObject(
144
+ {shallowField: {deepField: {oldField: null}}}
145
+ );
146
+ var afterIO = LegacyImmutableObject.set(beforeIO, {});
147
+ expect(afterIO).toBeSeriallyEqualTo(beforeIO);
148
+ expect(afterIO).not.toBe(beforeIO);
149
+ }
150
+ );
151
+
152
+ testDevAndProd(
153
+ 'should create distinct object with shallow field insertion',
154
+ function() {
155
+ var beforeStructure = {
156
+ oldShallowField: {
157
+ deepField: {
158
+ oldField: null
159
+ }
160
+ }
161
+ };
162
+
163
+ var delta = {
164
+ newShallowField: 'newShallowFieldHere'
165
+ };
166
+
167
+ var expectedAfterStructure = {
168
+ oldShallowField: {
169
+ deepField: {
170
+ oldField: null
171
+ }
172
+ },
173
+ newShallowField: 'newShallowFieldHere'
174
+ };
175
+
176
+ var beforeIO = new LegacyImmutableObject(beforeStructure);
177
+ var afterIO = LegacyImmutableObject.set(beforeIO, delta);
178
+ expect(afterIO).toBeSeriallyEqualTo(expectedAfterStructure);
179
+ expect(afterIO).not.toBe(beforeIO);
180
+ }
181
+ );
182
+
183
+ testDevAndProd(
184
+ 'should create distinct object with shallow field mutation',
185
+ function() {
186
+ var beforeStructure = {
187
+ oldShallowField: {
188
+ deepField: {
189
+ oldField: null
190
+ }
191
+ }
192
+ };
193
+
194
+ var delta = {
195
+ oldShallowField: 'this will clobber the old field'
196
+ };
197
+
198
+ var expectedAfterStructure = {
199
+ oldShallowField: 'this will clobber the old field'
200
+ };
201
+
202
+ var beforeIO = new LegacyImmutableObject(beforeStructure);
203
+ var afterIO = LegacyImmutableObject.set(beforeIO, delta);
204
+ expect(afterIO).toBeSeriallyEqualTo(expectedAfterStructure);
205
+ expect(afterIO).not.toBe(beforeIO);
206
+ }
207
+ );
208
+
209
+ message = 'should create distinct object with deep field insertion';
210
+ testDevAndProd(message, function() {
211
+ var beforeStructure = {
212
+ oldShallowField: {
213
+ deepField: {
214
+ oldField: null
215
+ }
216
+ }
217
+ };
218
+
219
+ var delta = {
220
+ oldShallowField: {newDeepField: 'hello'}
221
+ };
222
+
223
+ // LegacyImmutableObject does not yet support deep merging with
224
+ // LegacyImmutableObject.set().
225
+ var expectedAfterStructure = {
226
+ oldShallowField: {newDeepField: 'hello'}
227
+ };
228
+
229
+ var beforeIO = new LegacyImmutableObject(beforeStructure);
230
+ var afterIO = LegacyImmutableObject.set(beforeIO, delta);
231
+ expect(afterIO).toBeSeriallyEqualTo(expectedAfterStructure);
232
+ expect(afterIO).not.toBe(beforeIO);
233
+ });
234
+
235
+ message =
236
+ 'should tolerate arrays at deeper levels and prevent mutation on them';
237
+ testDevAndProd(message, function() {
238
+ if (window.callPhantom) {
239
+ // PhantomJS has a bug with Object.freeze and Arrays.
240
+ // https://github.com/ariya/phantomjs/issues/10817
241
+ return;
242
+ }
243
+ var beforeStructure = {
244
+ shallowField: [1,'second field',3]
245
+ };
246
+ var io = new LegacyImmutableObject(beforeStructure);
247
+ expect(function() {
248
+ io.newField = 'nope!';
249
+ }).toThrow();
250
+ expect(function() {
251
+ io.shallowField[0] = 'nope!';
252
+ }).toThrow();
253
+ expect(io.shallowField[1]).toEqual('second field');
254
+ });
255
+
256
+ message = 'should provide a setField interface as sugar for set()';
257
+ testDevAndProd(message, function() {
258
+ var beforeIO = new LegacyImmutableObject({initialField: null});
259
+ var afterIO =
260
+ LegacyImmutableObject.setField(beforeIO, 'anotherField', 'anotherValue');
261
+ expect(afterIO).toBeSeriallyEqualTo({
262
+ initialField: null,
263
+ anotherField: 'anotherValue'
264
+ });
265
+ expect(afterIO).not.toBe(beforeIO);
266
+ });
267
+
268
+ message = 'should recursively create distinct objects when deep copying';
269
+ testDevAndProd(message, function() {
270
+ var beforeIO = new LegacyImmutableObject({
271
+ a: {b: 'b', c: {}, d: 'd', e: new LegacyImmutableObject({f: 'f'}) }
272
+ });
273
+ var afterIO = LegacyImmutableObject.setDeep(beforeIO, {
274
+ a: {b: {}, c: 'C', e: {f: 'F', g: 'G'}, h: 'H'}
275
+ });
276
+ expect(afterIO).toBeSeriallyEqualTo({
277
+ a: {b: {}, c: 'C', d: 'd', e: {f: 'F', g: 'G'}, h: 'H'}
278
+ });
279
+ expect(afterIO).not.toBe(beforeIO);
280
+ expect(afterIO.a).not.toBe(beforeIO.a);
281
+ expect(afterIO.a.e).not.toBe(beforeIO.a.e);
282
+ });
283
+
284
+ testDevAndProd('should deep copy member immutability', function() {
285
+ var beforeIO = new LegacyImmutableObject({
286
+ a: {b: new LegacyImmutableObject({c: 'c'}), e: {f: 'f'}}
287
+ });
288
+ var afterIO = LegacyImmutableObject.setDeep(beforeIO, {
289
+ a: {b: {d: 'D'}, e: new LegacyImmutableObject({g: 'G'})}
290
+ });
291
+ expect(afterIO).toBeSeriallyEqualTo({
292
+ a: {b: {c: 'c', d: 'D'}, e: {f: 'f', g: 'G'}}
293
+ });
294
+ expect(afterIO instanceof LegacyImmutableObject).toBe(true);
295
+ expect(afterIO.a.b instanceof LegacyImmutableObject).toBe(true);
296
+ expect(afterIO.a.e instanceof LegacyImmutableObject).toBe(true);
297
+ });
298
+});