Sync latest Immutable changes

This commit is contained in:
Kunal Mehta
2014-03-18 14:57:04 -07:00
committed by Paul O’Shannessy
parent 8d495f3b6e
commit 0cec4af8d7
3 changed files with 147 additions and 188 deletions
+110 -143
View File
@@ -14,178 +14,145 @@
* limitations under the License.
*
* @providesModule ImmutableObject
* @typechecks
*/
"use strict";
var Immutable = require('Immutable');
var invariant = require('invariant');
var isNode = require('isNode');
var merge = require('merge');
var mergeInto = require('mergeInto');
var keyOf = require('keyOf');
var mergeHelpers = require('mergeHelpers');
var checkMergeObjectArgs = mergeHelpers.checkMergeObjectArgs;
var isTerminal = mergeHelpers.isTerminal;
/**
* Wrapper around JavaScript objects that provide a guarantee of immutability at
* developer time when strict mode is used. The extra computations required to
* enforce immutability is stripped out in production for performance reasons.
*/
var ImmutableObject;
var SECRET_KEY = keyOf({_DONT_EVER_TYPE_THIS_SECRET_KEY: null});
function assertImmutableObject(immutableObject) {
/**
* Static methods creating and operating on instances of `Immutable`.
*/
function assertImmutable(immutable) {
invariant(
immutableObject instanceof ImmutableObject,
immutable instanceof Immutable,
'ImmutableObject: Attempted to set fields on an object that is not an ' +
'instance of ImmutableObject.'
'instance of Immutable.'
);
}
if (__DEV__) {
/**
* Constructs an instance of `ImmutableObject`.
*
* @param {?object} initialProperties The initial set of properties.
* @constructor
*/
ImmutableObject = function ImmutableObject(initialProperties) {
mergeInto(this, initialProperties);
deepFreeze(this);
};
/**
* Checks if an object should be deep frozen. Instances of `ImmutableObject`
* are assumed to have already been deep frozen.
*
* @param {*} object The object to check.
* @return {boolean} Whether or not deep freeze is needed.
*/
var shouldRecurseFreeze = function(object) {
return (
typeof object === 'object' &&
!(object instanceof ImmutableObject) &&
object !== null
);
};
/**
* Freezes the supplied object deeply.
*
* @param {*} object The object to freeze.
*/
var deepFreeze = function(object) {
if (isNode(object)) {
return; // Don't try to freeze DOM nodes.
}
Object.freeze(object); // First freeze the object.
for (var prop in object) {
var field = object[prop];
if (object.hasOwnProperty(prop) && shouldRecurseFreeze(field)) {
deepFreeze(field);
}
}
};
/**
* Returns a new ImmutableObject that is identical to the supplied object but
* with the supplied changes, `put`.
*
* @param {ImmutableObject} immutableObject Starting object.
* @param {?object} put Fields to merge into the object.
* @return {ImmutableObject} The result of merging in `put` fields.
*/
ImmutableObject.set = function(immutableObject, put) {
assertImmutableObject(immutableObject);
var totalNewFields = merge(immutableObject, put);
return new ImmutableObject(totalNewFields);
};
} else {
/**
* Constructs an instance of `ImmutableObject`.
*
* @param {?object} initialProperties The initial set of properties.
* @constructor
*/
ImmutableObject = function ImmutableObject(initialProperties) {
mergeInto(this, initialProperties);
};
/**
* Returns a new ImmutableObject that is identical to the supplied object but
* with the supplied changes, `put`.
*
* @param {ImmutableObject} immutableObject Starting object.
* @param {?object} put Fields to merge into the object.
* @return {ImmutableObject} The result of merging in `put` fields.
*/
ImmutableObject.set = function(immutableObject, put) {
assertImmutableObject(immutableObject);
var newMap = new ImmutableObject(immutableObject);
mergeInto(newMap, put);
return newMap;
};
}
/**
* Sugar for `ImmutableObject.set(ImmutableObject, {fieldName: putField})`.
*
* @param {ImmutableObject} immutableObject Object on which to set field.
* @param {string} fieldName Name of the field to set.
* @param {*} putField Value of the field to set.
* @return {ImmutableObject} [description]
* Static methods for reasoning about instances of `ImmutableObject`. Execute
* the freeze commands in `__DEV__` mode to alert the programmer that something
* is attempting to mutate. Since freezing is very expensive, we avoid doing it
* at all in production.
*/
ImmutableObject.setField = function(immutableObject, fieldName, putField) {
var put = {};
put[fieldName] = putField;
return ImmutableObject.set(immutableObject, put);
};
/**
* Returns a new ImmutableObject that is identical to the supplied object but
* with the supplied changes recursively applied.
*
* @param {ImmutableObject} immutableObject Object on which to set fields.
* @param {object} put Fields to merge into the object.
* @return {ImmutableObject} The result of merging in `put` fields.
*/
ImmutableObject.setDeep = function(immutableObject, put) {
assertImmutableObject(immutableObject);
return _setDeep(immutableObject, put);
};
function _setDeep(object, put) {
checkMergeObjectArgs(object, put);
var totalNewFields = {};
// To maintain the order of the keys, copy the base object's entries first.
var keys = Object.keys(object);
for (var ii = 0; ii < keys.length; ii++) {
var key = keys[ii];
if (!put.hasOwnProperty(key)) {
totalNewFields[key] = object[key];
} else if (isTerminal(object[key]) || isTerminal(put[key])) {
totalNewFields[key] = put[key];
} else {
totalNewFields[key] = _setDeep(object[key], put[key]);
class ImmutableObject extends Immutable {
/**
* @arguments {array<object>} The arguments is an array of objects that, when
* merged together, will form the immutable objects.
*/
constructor() {
super(Immutable[SECRET_KEY]);
Immutable.mergeAllPropertiesInto(this, arguments);
if (__DEV__) {
Immutable.deepFreezeRootNode(this);
}
}
// Apply any new keys that the base object didn't have.
/**
* DEPRECATED - prefer to instantiate with new ImmutableObject().
*
* @arguments {array<object>} The arguments is an array of objects that, when
* merged together, will form the immutable objects.
*/
static create() {
var obj = Object.create(ImmutableObject.prototype);
ImmutableObject.apply(obj, arguments);
return obj;
}
/**
* Returns a new `Immutable` that is identical to the supplied `Immutable`
* but with the specified changes, `put`. Any keys that are in the
* intersection of `immutable` and `put` retain the ordering of `immutable.
* New keys are placed after keys that exist in `immutable`.
*
* @param {Immutable} immutable Starting object.
* @param {?object} put Fields to merge into the object.
* @return {Immutable} The result of merging in `put` fields.
*/
static set(immutable, put) {
assertImmutable(immutable);
invariant(
typeof put === 'object' && put !== undefined && !Array.isArray(put),
'Invalid ImmutableMap.set argument `put`'
);
return new ImmutableObject(immutable, put);
}
/**
* Sugar for `ImmutableObject.set(ImmutableObject, {fieldName: putField})`.
* Look out for key crushing: Use `keyOf()` to guard against it.
*
* @param {Immutable} immutable Object on which to set properties.
* @param {string} fieldName Name of the field to set.
* @param {*} putField Value of the field to set.
* @return {Immutable} new Immutable as described in `set`.
*/
static setProperty(immutableObject, fieldName, putField) {
var put = {};
put[fieldName] = putField;
return ImmutableObject.set(immutableObject, put);
}
/**
* Returns a new `Immutable` that is identical to the supplied object but
* with the supplied changes recursively applied.
*
* Experimental. Likely does not handle `Arrays` correctly.
*
* @param {Immutable} immutable Object on which to set fields.
* @param {object} put Fields to merge into the object.
* @return {Immutable} The result of merging in `put` fields.
*/
static setDeep(immutable, put) {
assertImmutable(immutable);
return _setDeep(immutable, put);
}
}
function _setDeep(obj, put) {
checkMergeObjectArgs(obj, put);
var totalNewFields = {};
// To maintain the order of the keys, copy the base object's entries first.
var keys = Object.keys(obj);
for (var ii = 0; ii < keys.length; ii++) {
var key = keys[ii];
if (!put.hasOwnProperty(key)) {
totalNewFields[key] = obj[key];
} else if (isTerminal(obj[key]) || isTerminal(put[key])) {
totalNewFields[key] = put[key];
} else {
totalNewFields[key] = _setDeep(obj[key], put[key]);
}
}
// Apply any new keys that the base obj didn't have.
var newKeys = Object.keys(put);
for (ii = 0; ii < newKeys.length; ii++) {
var newKey = newKeys[ii];
if (object.hasOwnProperty(newKey)) {
if (obj.hasOwnProperty(newKey)) {
continue;
}
totalNewFields[newKey] = put[newKey];
}
return (object instanceof ImmutableObject || put instanceof ImmutableObject) ?
new ImmutableObject(totalNewFields) :
totalNewFields;
return (
obj instanceof Immutable ? new ImmutableObject(totalNewFields) :
put instanceof Immutable ? new ImmutableObject(totalNewFields) :
totalNewFields
);
}
module.exports = ImmutableObject;
+35 -43
View File
@@ -18,10 +18,8 @@
"use strict";
require('mock-modules')
.dontMock('ImmutableObject');
var ImmutableObject;
var Immutable;
/**
* To perform performance testing of using `ImmutableObject` vs. not using
@@ -30,40 +28,27 @@ var ImmutableObject;
describe('ImmutableObject', function() {
var message;
beforeEach(function() {
require('mock-modules').dumpCache();
ImmutableObject = require('ImmutableObject');
this.addMatchers({
/**
* Equivalent with respect to serialization. Must stringify because
* constructors are different and other comparison methods will not
* consider them structurally equal. Probably not useful for use outside
* of this test module.
*/
toBeSeriallyEqualTo: function(expected) {
var actual = this.actual;
var notText = this.isNot ? " not" : "";
this.message = function () {
return "Expected " + JSON.stringify(actual) + notText +
" to be serially equal to " + JSON.stringify(expected);
};
return JSON.stringify(actual) === JSON.stringify(expected);
}
});
Immutable = require('Immutable');
});
/**
* We are in __DEV__ by default.
*/
var testDev = function(message, testFunc) {
it(message, testFunc);
it(message, function() {
var old = window.__DEV__;
window.__DEV__ = true;
testFunc();
window.__DEV__ = old;
});
};
var testProd = function(message, testFunc) {
// Temporarily enter production mode
window.__DEV__ = false;
it(message, testFunc);
window.__DEV__ = true;
it(message, function() {
// Temporarily enter production mode
var old = window.__DEV__;
window.__DEV__ = false;
testFunc();
window.__DEV__ = old;
});
};
var testDevAndProd = function(message, testFunc) {
@@ -89,6 +74,12 @@ describe('ImmutableObject', function() {
}).not.toThrow();
});
testDevAndProd('should extend Immutable', function() {
var object = new ImmutableObject({foo: 'bar'});
expect (object instanceof Immutable).toBe(true);
expect (object instanceof ImmutableObject).toBe(true);
});
testDev('should not exceed maximum call stack size with nodes', function() {
var node = document.createElement('div');
var object = new ImmutableObject({node: node});
@@ -142,7 +133,7 @@ describe('ImmutableObject', function() {
var beforeIO =
new ImmutableObject({shallowField: {deepField: {oldField: null}}});
var afterIO = ImmutableObject.set(beforeIO, {});
expect(afterIO).toBeSeriallyEqualTo(beforeIO);
expect(afterIO).toSeriallyEqual(beforeIO);
expect(afterIO).not.toBe(beforeIO);
}
);
@@ -173,7 +164,7 @@ describe('ImmutableObject', function() {
var beforeIO = new ImmutableObject(beforeStructure);
var afterIO = ImmutableObject.set(beforeIO, delta);
expect(afterIO).toBeSeriallyEqualTo(expectedAfterStructure);
expect(afterIO).toSeriallyEqual(expectedAfterStructure);
expect(afterIO).not.toBe(beforeIO);
}
);
@@ -199,7 +190,7 @@ describe('ImmutableObject', function() {
var beforeIO = new ImmutableObject(beforeStructure);
var afterIO = ImmutableObject.set(beforeIO, delta);
expect(afterIO).toBeSeriallyEqualTo(expectedAfterStructure);
expect(afterIO).toSeriallyEqual(expectedAfterStructure);
expect(afterIO).not.toBe(beforeIO);
}
);
@@ -226,13 +217,13 @@ describe('ImmutableObject', function() {
var beforeIO = new ImmutableObject(beforeStructure);
var afterIO = ImmutableObject.set(beforeIO, delta);
expect(afterIO).toBeSeriallyEqualTo(expectedAfterStructure);
expect(afterIO).toSeriallyEqual(expectedAfterStructure);
expect(afterIO).not.toBe(beforeIO);
});
message =
'should tolerate arrays at deeper levels and prevent mutation on them';
testDevAndProd(message, function() {
testDev(message, function() {
if (window.callPhantom) {
// PhantomJS has a bug with Object.freeze and Arrays.
// https://github.com/ariya/phantomjs/issues/10817
@@ -251,12 +242,12 @@ describe('ImmutableObject', function() {
expect(io.shallowField[1]).toEqual('second field');
});
message = 'should provide a setField interface as sugar for set()';
message = 'should provide a setProperty interface as sugar for set()';
testDevAndProd(message, function() {
var beforeIO = new ImmutableObject({initialField: null});
var afterIO =
ImmutableObject.setField(beforeIO, 'anotherField', 'anotherValue');
expect(afterIO).toBeSeriallyEqualTo({
ImmutableObject.setProperty(beforeIO, 'anotherField', 'anotherValue');
expect(afterIO).toSeriallyEqual({
initialField: null,
anotherField: 'anotherValue'
});
@@ -271,7 +262,7 @@ describe('ImmutableObject', function() {
var afterIO = ImmutableObject.setDeep(beforeIO, {
a: {b: {}, c: 'C', e: {f: 'F', g: 'G'}, h: 'H'}
});
expect(afterIO).toBeSeriallyEqualTo({
expect(afterIO).toSeriallyEqual({
a: {b: {}, c: 'C', d: 'd', e: {f: 'F', g: 'G'}, h: 'H'}
});
expect(afterIO).not.toBe(beforeIO);
@@ -286,11 +277,12 @@ describe('ImmutableObject', function() {
var afterIO = ImmutableObject.setDeep(beforeIO, {
a: {b: {d: 'D'}, e: new ImmutableObject({g: 'G'})}
});
expect(afterIO).toBeSeriallyEqualTo({
expect(afterIO).toSeriallyEqual({
a: {b: {c: 'c', d: 'D'}, e: {f: 'f', g: 'G'}}
});
expect(afterIO instanceof ImmutableObject).toBe(true);
expect(afterIO.a.b instanceof ImmutableObject).toBe(true);
expect(afterIO.a.e instanceof ImmutableObject).toBe(true);
expect(afterIO instanceof Immutable).toBe(true);
expect(afterIO.a.b instanceof Immutable).toBe(true);
expect(afterIO.a.e instanceof Immutable).toBe(true);
});
});
+2 -2
View File
@@ -1,5 +1,5 @@
/**
* Copyright 2014 Facebook, Inc.
* Copyright 2013-2014 Facebook, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -61,7 +61,7 @@ class Immutable {
/**
* Helper method for classes that make use of `Immutable`.
* @param {Immutable} immutable Object to merge properties into.
* @param {Array<object>} propertyObjects List of objects to merge into
* @param {array<object>} propertyObjects List of objects to merge into
* `destination`.
*/
static mergeAllPropertiesInto(destination, propertyObjects) {