@samitouri / QOS-React-2 / commits / 4ad320dd13

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 +});