Source: index.js

/**
 * @module mixing 
 */

/*jshint latedef:nofunc*/

// Load shim only in browser environment. Array.isArray is defined in node
if (! Array.isArray) {
    require("isarray-shim");
}

function prepareFieldList(fieldList) {
    var map, nI, regexp, sType;
    if (Array.isArray(fieldList)) {
        if (fieldList.length > 0) {
            map = {};
            nI = fieldList.length;
            do {
                map[ fieldList[--nI] ] = null;
            } while(nI);
        }
    }
    else {
        sType = typeof fieldList;
        if (sType === "string" || sType === "symbol") {
            map = {};
            map[fieldList] = null;
        }
        else if (sType === "object") {
            if (fieldList instanceof RegExp) {
                regexp = fieldList;
            }
            else {
                map = fieldList;
            }
        }
    }
    return {map: map, regexp: regexp};
}

function copy(destination, source, propName, settings) {
    /*jshint laxbreak:true*/
    var propValue = source[propName],
        sPropString = propName.toString(),
        bFuncProp, change, otherNameMap, sType, value;
    if ((! settings.ownProperty || source.hasOwnProperty(propName))
            && (! settings.copyMap || (propName in settings.copyMap))
            && (! settings.copyRegExp || settings.copyRegExp.test(sPropString))
            && (! settings.exceptions || ! (propName in settings.exceptions))
            && (! settings.exceptRegExp || ! settings.exceptRegExp.test(sPropString))
            && (! settings.filter || settings.filter.call(null, {field: propName, value: propValue, target: destination, source: source}))
            && (! settings.filterRegExp || settings.filterRegExp.test(typeof propValue === "symbol" ? propValue.toString() : propValue))) {
        otherNameMap = settings.otherNameMap;
        if (otherNameMap && (propName in otherNameMap)) {
            propName = otherNameMap[propName];
        }
        sType = typeof propValue;
        // If recursive mode and field's value is an object
        if (settings.recursive && propValue && sType === "object" && (value = destination[propName]) && typeof value === "object"
                && (! Array.isArray(propValue) || settings.mixFromArray) && (! Array.isArray(value) || settings.mixToArray)) {
            mixing(value, propValue,
                    settings.mixFromArray
                        ? mixing({oneSource: true}, settings)
                        : settings);
        }
        else {
            bFuncProp = (sType === "function");
            if ((settings.overwrite || ! (propName in destination))
                    && (! bFuncProp || settings.copyFunc)) {
                if (settings.changeFunc) {
                    propValue = settings.changeFunc.call(null, {field: propName, value: propValue, target: destination, source: source});
                }
                else if ((change = settings.change) && (propName in change)) {
                    propValue = change[propName];
                }
                if (bFuncProp && settings.funcToProto) {
                    destination.constructor.prototype[propName] = propValue;
                }
                else {
                    destination[propName] = propValue;
                }
            }
        }
    }
}

/**
 * Copy/add all fields and functions from source objects into the target object.
 * As a result the target object may be modified.
 *
 * @param {Object} destination
 *      The target object into which fields and functions will be copied.
 * @param {Array | Object} source
 *      Array of source objects or just one object whose contents will be copied.
 *      If a source is a falsy value (e.g. <code>null</code> or <code>undefined</code>), the source will be skipped.
 * @param {Object} [settings]
 *      Operation settings. Fields are names of settings, their values are the corresponding values of settings.
 *      The following settings are being supported.
 *      <table>
 *          <tr>
 *              <th>Name</th><th>Type</th><th>Default value</th><th>Description</th>
 *          </tr>
 *          <tr>
 *              <td><code>copyFunc</code></td>
 *              <td><code>Boolean</code></td>
 *              <td><code>true</code></td>
 *              <td>Should functions be copied?</td>
 *          </tr>
 *          <tr>
 *              <td><code>funcToProto</code></td>
 *              <td><code>Boolean</code></td>
 *              <td><code>false</code></td>
 *              <td>
 *                  Should functions be copied into <code>prototype</code> of the target object's <code>constructor</code>
 *                  (i.e. into <code>destination.constructor.prototype</code>)?
 *                  <br>
 *                  If <code>false</code> then functions will be copied directly into the target object.
 *              </td>
 *          </tr>
 *          <tr>
 *              <td><code>processSymbol</code></td>
 *              <td><code>Boolean</code></td>
 *              <td><code>true</code></td>
 *              <td>Should symbol property keys (i.e. fields whose name is a symbol) be processed?</td>
 *          </tr>
 *          <tr>
 *              <td><code>overwrite</code></td>
 *              <td><code>Boolean</code></td>
 *              <td><code>false</code></td>
 *              <td>Should a field/function be overwritten when it exists in the target object?</td>
 *          </tr>
 *          <tr>
 *              <td><code>recursive</code></td>
 *              <td><code>Boolean</code></td>
 *              <td><code>false</code></td>
 *              <td>
 *                  Should this function be called recursively when field's value of the target and source object is an object?
 *                  <br>
 *                  If <code>true</code> then object fields from the target and source objects will be mixed by using this function
 *                  with the same settings.
 *                  <br>
 *                  This option has no dependency with <code>overwrite</code> setting and has priority over it.
 *              </td>
 *          </tr>
 *          <tr>
 *              <td><code>mixFromArray</code></td>
 *              <td><code>Boolean</code></td>
 *              <td><code>false</code></td>
 *              <td>
 *                  Should contents of a field of the source object be copied when the field's value is an array?
 *                  <br>
 *                  Will be used only when <code>recursive</code> setting has <code>true</code> value.
 *              </td>
 *          </tr>
 *          <tr>
 *              <td><code>mixToArray</code></td>
 *              <td><code>Boolean</code></td>
 *              <td><code>false</code></td>
 *              <td>
 *                  Should contents of a field of the source object be copied into a field of the target object
 *                  when the latest field's value is an array?
 *                  <br>
 *                  Will be used only when <code>recursive</code> setting has <code>true</code> value.
 *              </td>
 *          </tr>
 *          <tr>
 *              <td><code>oneSource</code></td>
 *              <td><code>Boolean</code></td>
 *              <td><code>false</code></td>
 *              <td>
 *                  Indicates that array that is passed as <code>source</code> parameter should be interpreted
 *                  directly as copied object instead of list of source objects.
 *              </td>
 *          </tr>
 *          <tr>
 *              <td><code>ownProperty</code></td>
 *              <td><code>Boolean</code></td>
 *              <td><code>false</code></td>
 *              <td>Should only own properties of the source object be copied in the target object?</td>
 *          </tr>
 *          <tr>
 *              <td><code>copy</code></td>
 *              <td><code>Array | Object | RegExp | String | Symbol</code></td>
 *              <td><code>""</code> (empty string)</td>
 *              <td>
 *                  Array, object, regular expression or string/symbol that defines names of fields/functions that should be copied.
 *                  <br>
 *                  If an object is passed then his fields determine copied elements.
 *                  If a regular expression is passed, then field names matching the regular expression will be copied.
 *                  If a string/symbol is passed then it is name of the only copied field.
 *              </td>
 *          </tr>
 *          <tr>
 *              <td><code>except</code></td>
 *              <td><code>Array | Object | RegExp | String | Symbol</code></td>
 *              <td><code>""</code> (empty string)</td>
 *              <td>
 *                  Array, object, regular expression or string/symbol that defines names of fields/functions that shouldn't be copied.
 *                  <br>
 *                  If an object is passed then his fields determine non-copied elements.
 *                  If a regular expression is passed, then field names matching the regular expression will not be copied.
 *                  If a string/symbol is passed then it is name of the only non-copied field.
 *              </td>
 *          </tr>
 *          <tr>
 *              <td><code>filter</code></td>
 *              <td><code>Function | RegExp</code></td>
 *              <td><code>null</code></td>
 *              <td>
 *                  Function or regular expression that can be used to select elements that should be copied.
 *                  <br>
 *                  If regular expression is passed, only those fields will be copied whose values are matching regular expression.
 *                  <br>
 *                  If specified function returns <code>true</code> for a field,
 *                  the field will be copied in the target object.
 *                  <br>
 *                  An object having the following fields is passed into filter function:
 *                  <ul>
 *                  <li><code>field</code> - field name
 *                  <li><code>value</code> - field value
 *                  <li><code>target</code> - reference to the target object
 *                  <li><code>source</code> - reference to the source object
 *                  </ul>
 *              </td>
 *          </tr>
 *          <tr>
 *              <td><code>otherName</code></td>
 *              <td><code>Object</code></td>
 *              <td><code>null</code></td>
 *              <td>
 *                  Defines "renaming table" for copied elements.
 *                  <br>
 *                  Fields of the table are names from a source object, values are the corresponding names in the target object.
 *                  <br>
 *                  For example, the call
 *                  <br>
 *                  <code>
 *                  mixing({}, {field: 1, func: "no-func"}, {otherName: {"field": "prop", "func": "method"}})
 *                  </code>
 *                  <br>
 *                  will return the following object
 *                  <br>
 *                  <code>{prop: 1, method: "no-func"}</code>
 *              </td>
 *          </tr>
 *          <tr>
 *              <td><code>change</code></td>
 *              <td><code>Function | Object</code></td>
 *              <td><code>null</code></td>
 *              <td>
 *                  Function or object that gives ability to change values that should be copied.
 *                  <br>
 *                  If an object is passed then his fields determine new values for copied elements.
 *                  <br>
 *                  If a function is passed then value returned by the function for a field will be copied into the target object 
 *                  instead of original field's value.
 *                  <br>
 *                  An object having the following fields is passed into change function:
 *                  <ul>
 *                  <li><code>field</code> - field name
 *                  <li><code>value</code> - field value
 *                  <li><code>target</code> - reference to the target object
 *                  <li><code>source</code> - reference to the source object
 *                  </ul>
 *              </td>
 *          </tr>
 *      </table>
 *      <code>copy</code>, <code>except</code> and <code>filter</code> settings can be used together.
 *      In such situation a field will be copied only when the field satisfies to all settings
 *      (i.e. belongs to copied elements, not in exceptions and conforms to filter).
 * @return {Object}
 *      Modified target object.
 * @alias module:mixing
 */
function mixing(destination, source, settings) {
    /*jshint boss:true, laxbreak:true*/
    if (typeof source === "object" && source !== null) {
        // Prepare parameters
        if (typeof settings !== "object" || settings === null) {
            settings = {};
        }
        if (! Array.isArray(source) || settings.oneSource) {
            source = [source];
        }
        // Prepare settings
        var getOwnPropertySymbols = Object.getOwnPropertySymbols,
            getPrototypeOf = Object.getPrototypeOf,
            options = {
                copyFunc: ("copyFunc" in settings ? settings.copyFunc : true),
                funcToProto: Boolean(settings.funcToProto),
                processSymbol: ("processSymbol" in settings ? settings.processSymbol : true)
                                    && typeof getOwnPropertySymbols === "function",
                mixFromArray: Boolean(settings.mixFromArray),
                mixToArray: Boolean(settings.mixToArray),
                overwrite: Boolean(settings.overwrite),
                ownProperty: Boolean(settings.ownProperty),
                recursive: Boolean(settings.recursive),
                otherNameMap: ("otherName" in settings ? settings.otherName : null)
            },
            bOwnProperty = options.ownProperty,
            bProcessSymbol = options.processSymbol,
            change = settings.change,
            copyList = settings.copy,
            exceptList = settings.except,
            filter = settings.filter,
            nI, nK, nL, nQ, obj, propName;
        
        if (copyList) {
            copyList = prepareFieldList(copyList);
            options.copyMap = copyList.map;
            options.copyRegExp = copyList.regexp;
        }
        if (exceptList) {
            exceptList = prepareFieldList(exceptList);
            options.exceptions = exceptList.map;
            options.exceptRegExp = exceptList.regexp;
        }
        if (filter) {
            options[typeof filter === "object" ? "filterRegExp" : "filter"] = filter;
        }
        if (change) {
            options[typeof change === "function" ? "changeFunc" : "change"] = change;
        }
        
        // Copy fields and functions according to settings
        for (nI = 0, nL = source.length; nI < nL; nI++) {
            if (obj = source[nI]) {
                for (propName in obj) {
                    copy(destination, obj, propName, options);
                }
                // Process symbol property keys
                if (bProcessSymbol) {
                    exceptList = {};
                    do {
                        copyList = getOwnPropertySymbols(obj);
                        for (nK = 0, nQ = copyList.length; nK < nQ; nK++) {
                            propName = copyList[nK];
                            if (! (propName in exceptList)) {
                                copy(destination, obj, propName, options);
                                exceptList[propName] = true;
                            }
                        }
                        obj = bOwnProperty
                                ? null
                                : getPrototypeOf(obj);
                    } while (obj);
                }
            }
        }
    }
    return destination;
}

/**
 * Change values of fields of given object.
 * <br>
 * This function is a "wrap" for the following code:
 * <code><pre>
 * mixing(source, source, {change: change, overwrite: true, oneSource: true});
 * </pre></code>
 * 
 * @param {Array | Object} source
 *      An array or an object whose fields should be modified.
 * @param {Function | Object} change
 *      A function or an object that specifies the modification. See {@link module:mixing mixing} for details.
 * @return {Object}
 *      Modified <code>source</code> object.
 */
mixing.change = function(source, change) {
    return mixing(source, source, {change: change, overwrite: true, oneSource: true});
};

/**
 * Make a copy of source object(s).
 * <br>
 * This function is a "wrap" for the following code:
 * <code><pre>
 * var copy = mixing({}, source, settings);
 * </pre></code>
 * 
 * @param {Array | Object} source
 *      Array of source objects or just one object whose contents will be copied.
 * @param {Object} [settings]
 *      Operation settings. See {@link module:mixing mixing} for details.
 * @return {Object}
 *      Newly created object containing contents of source objects.
 */
mixing.copy = function(source, settings) {
    return mixing({}, source, settings);
};

/**
 * Make a copy of <code>this</code> object.
 * <br>
 * This function is a "wrap" for the following code:
 * <code><pre>
 * var copy = mixing({}, this, settings);
 * </pre></code>
 * It can be transferred to an object to use as a method.
 * 
 * @param {Object} [settings]
 *      Operation settings. See {@link module:mixing mixing} for details.
 * @return {Object}
 *      Newly created object containing contents of <code>this</code> object.
 */
mixing.clone = function(settings) {
    return mixing({}, this, settings);
};

/**
 * Filter <code>this</code> object.
 * <br>
 * This function is a "wrap" for the following code:
 * <code><pre>
 * var result = mixing({}, this, {filter: filter});
 * </pre></code>
 * It can be transferred to an object to use as a method.
 * 
 * @param {Function | Object} filter
 *      Filter function to select fields or object that represents operation settings including filter function.
 *      See {@link module:mixing mixing} for details.
 * @return {Object}
 *      Newly created object containing fields of <code>this</code> object for which filter function returns true.
 */
mixing.filter = function(filter) {
    return mixing({}, this, typeof filter === "function" ? {filter: filter} : filter);
};

/**
 * Copy and change values of fields of <code>this</code> object.
 * <br>
 * This function is a "wrap" for the following code:
 * <code><pre>
 * var result = mixing({}, this, {change: change});
 * </pre></code>
 * It can be transferred to an object to use as a method.
 * 
 * @param {Function | Object} change
 *      Function to change values of copied fields or object that represents operation settings including change function.
 *      See {@link module:mixing mixing} for details.
 * @return {Object}
 *      Newly created object containing fields of <code>this</code> object with changed values.
 */
mixing.map = function(change) {
    return mixing({}, this, typeof change === "function" ? {change: change} : change);
};

/**
 * Copy/add all fields and functions from source objects into <code>this</code> object.
 * As a result <code>this</code> object may be modified.
 * <br>
 * This function is a "wrap" for the following code:
 * <code><pre>
 * mixing(this, source, settings);
 * </pre></code>
 * It can be transferred to an object to use as a method.
 * 
 * @param {Array | Object} source
 *      Array of source objects or just one object whose contents will be copied.
 * @param {Object} [settings]
 *      Operation settings. See {@link module:mixing mixing} for details.
 * @return {Object}
 *      Modified <code>this</code> object.
 */
mixing.mix = function(source, settings) {
    return mixing(this, source, settings);
};

/**
 * Change values of fields of <code>this</code> object.
 * <br>
 * This function is a "wrap" for the following code:
 * <code><pre>
 * mixing.change(this, change);
 * </pre></code>
 * It can be transferred to an object to use as a method.
 * 
 * @param {Function | Object} change
 *      A function or an object that specifies the modification. See {@link module:mixing mixing} for details.
 * @return {Object}
 *      Modified <code>this</code> object.
 */
mixing.update = function(change) {
    return mixing.change(this, change);
};

module.exports = mixing;