/** * 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 ImmutableObject * @typechecks */ "use strict"; var invariant = require('invariant'); var isNode = require('isNode'); var merge = require('merge'); var mergeInto = require('mergeInto'); 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; function assertImmutableObject(immutableObject) { invariant( immutableObject instanceof ImmutableObject, 'ImmutableObject: Attempted to set fields on an object that is not an ' + 'instance of ImmutableObject.' ); } 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] */ 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]); } } // Apply any new keys that the base object didn't have. var newKeys = Object.keys(put); for (ii = 0; ii < newKeys.length; ii++) { var newKey = newKeys[ii]; if (object.hasOwnProperty(newKey)) { continue; } totalNewFields[newKey] = put[newKey]; } return (object instanceof ImmutableObject || put instanceof ImmutableObject) ? new ImmutableObject(totalNewFields) : totalNewFields; } module.exports = ImmutableObject;