/**
* @module mixing
*/
// 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") {
map = {};
map[fieldList] = null;
}
else if (sType === "object") {
if (fieldList instanceof RegExp) {
regexp = fieldList;
}
else {
map = fieldList;
}
}
}
return {map: map, regexp: regexp};
}
/**
* 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>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</code></td>
* <td><code>""</code> (empty string)</td>
* <td>
* Array, object, regular expression or string 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 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</code></td>
* <td><code>""</code> (empty string)</td>
* <td>
* Array, object, regular expression or string 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 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 bCopyFunc = ("copyFunc" in settings ? settings.copyFunc : true),
bFuncToProto = Boolean(settings.funcToProto),
bMixFromArray = Boolean(settings.mixFromArray),
bMixToArray = Boolean(settings.mixToArray),
bOverwrite = Boolean(settings.overwrite),
bOwnProperty = Boolean(settings.ownProperty),
bRecursive = Boolean(settings.recursive),
filter = settings.filter,
otherNameMap = ("otherName" in settings ? settings.otherName : null),
copyList = settings.copy,
exceptList = settings.except,
bFuncProp, change, changeFunc, copyMap, copyRegExp, exceptions, exceptRegExp, filterRegExp,
nI, nL, obj, propName, propValue, sType, value;
if (copyList) {
copyList = prepareFieldList(copyList);
copyMap = copyList.map;
copyRegExp = copyList.regexp;
}
if (exceptList) {
exceptList = prepareFieldList(exceptList);
exceptions = exceptList.map;
exceptRegExp = exceptList.regexp;
}
if (filter && typeof filter === "object") {
filterRegExp = filter;
filter = null;
}
if (settings.change) {
if (typeof settings.change === "function") {
changeFunc = settings.change;
}
else {
change = settings.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) {
propValue = obj[propName];
if ((! bOwnProperty || obj.hasOwnProperty(propName))
&& (! copyMap || (propName in copyMap))
&& (! copyRegExp || copyRegExp.test(propName))
&& (! exceptions || ! (propName in exceptions))
&& (! exceptRegExp || ! exceptRegExp.test(propName))
&& (! filter || filter({field: propName, value: propValue, target: destination, source: obj}))
&& (! filterRegExp || filterRegExp.test(propValue))) {
if (otherNameMap && (propName in otherNameMap)) {
propName = otherNameMap[propName];
}
sType = typeof propValue;
// If recursive mode and field's value is an object
if (bRecursive && propValue && sType === "object" && (value = destination[propName]) && typeof value === "object"
&& (! Array.isArray(propValue) || bMixFromArray) && (! Array.isArray(value) || bMixToArray)) {
mixing(value, propValue,
bMixFromArray
? mixing({oneSource: true}, settings)
: settings);
}
else {
bFuncProp = (sType === "function");
if ((bOverwrite || ! (propName in destination))
&& (! bFuncProp || bCopyFunc)) {
if (changeFunc) {
propValue = changeFunc({field: propName, value: propValue, target: destination, source: obj});
}
else if (change && (propName in change)) {
propValue = change[propName];
}
if (bFuncProp && bFuncToProto) {
destination.constructor.prototype[propName] = propValue;
}
else {
destination[propName] = propValue;
}
}
}
}
}
}
}
}
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;