@wider/utils_proto
Version:
A set of extensions to basic objects giving uniform behaviour in various technical environments
229 lines (204 loc) • 10.4 kB
JavaScript
'use strict';
/**
* Additional methods for the javascript **Function** object
* @module @wider/utils_proto/proto_object
*
* @copyright Copyright (C) 1985..2021 Martin Baker. http://y-d-r.co.uk
* @author Martin W Baker
* @license ISC 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.
*/
const $moduleName = "@wider/utils_proto/proto_object";
const objectPrototype = {};
/**
* Perform a deep merge of the current object with another object revising the current object
*
* A scan is made of all the [enumerable](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/propertyIsEnumerable) [own properties](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/hasOwnProperty) of the given ** mergeItems ** and the action is chosen as below
for each result found
*
* * if the result is ***undefined*** the value is deleted from the current object if it already exists
*
* * if the result is another native {Object} it will also have a `wider_deepMerge()` method - this is called recursively to merge into the matching keyName of the current item (creating the keyName's empty object first if one does not yet exist)
*
* * if the result is any other object the object itself is placed as this current value
*
* * for any other value, eg string or function, the keyName is set to the value, retaining the defined properties of the new value (eg immutability, readonly, etc)
*
* Important, child objects being deepMerged are never linked as the entire object - only enumerable members are processed. This means that the **mergeItems** object will never reflect changes in the current item and changes in the current item will not be reflected into the **mergeItems**
*
* This does not perform the same impact as the package "npm install deepMerge" - the main differences are that object.wider_deepMerge():
*
* * is this faster or significantly faster
*
* * updates your original object in situ so that any prior references to objects in it now refer to same object with possibly amended properties
*
* * has control as to whether matching keyNames defer or overwrite
*
* * has control over error handling
*
* * has different management of merging arrays
* @function deepMerge()
* @param {Object} mergeItems new items to merge into the current object.
* @param {object} [settings] settings that impact the behaviour - to use more than one value on the same key use a space separated list
* * `{ array : "push"}` if the new and the old values are arrays the new values are pushed into the array. As this always creates new elements in the array, this overrides the action of **mode** for arrays
* * `{ array : "pushUnique"}` as for ***push*** except that duplicate values are discarded
* * `{ array : "unshift"}` if the new and the old values are arrays the new values are unshifted into the array. This overrides the action of **mode** for arrays
* * `{ array : "unshiftUnique"}` as for ***push*** except that duplicate values are discarded
* * `{ error : fn(Error) } if an error occurs it is sent to the function (eg ***console.error***) and not, as per default, thrown. The value passes is the same error object that would have been thrown
* * `{ mode : 'defer' } if a value appears in the **mergeItems** that is already in original object, the original object is not updated - the default is **overwrite***
* * '{ custom : {handler : fn(path,value), pattern : regExp]}` if the path to the item matches the regExp the callback is used to process the behaviour
* @returns {Object} the current object - use of this return value is optional
* @throws {Error} when any of the following occur
* * if the **mergeItem** is not a javascript native {Object}
* * if the value of a **mergeItem** keyName is write only
* * if the value in the current object of keyName is immutable or readOnly AND is different from the value being supplied
* * if you use a combination of settings that when used are incompatible
*
* If an error is thrown and not trapped by your error setting function, your initial object will have been left partially updated
*/
const methods = {
"wider_deepMerge": {
enumerable: false,
value: function (mergeItems = {}, settings = {}, path = "") {
const $functionName = "object.prototype.wider_deepMerge";
if (!mergeItems.wider_deepMerge)
throw new Error($functionName + " - mergeItems must be a constructed from the javascript `Object` object");
for (const key in mergeItems) {
if (Object.hasOwnProperty.call(mergeItems, key)) {
const valuePath = path + "/" + key;
const value = mergeItems[key];
//TODO if the given getProperties obedience
try {
if (value === undefined)
delete this[key];
else if (settings.custom && settings.custom.pattern.test(valuePath))
this[key] = settings.custom.handler(valuePath, value);
else if (this[key] === undefined)
this[key] = value;
else if (this[key] === value ||
settings.mode === "defer" && !settings.array
) {
/*
skip if already exists and we are in defer mode or
is already the same object
*/
} //
else if (Array.isArray(this[key])) {
switch (settings.array) {
case "push":
if (Array.isArray(value))
value.forEach(element => {
this[key].push(element);
});
else
this[key].push(value);
break;
case "unshift":
if (Array.isArray(value))
this[key].unshift(...value);
else
this[key].unshift(value);
break;
case "pushUnique":
case "unshiftUnique":
if (Array.isArray(value))
/* jshint -W083 */
value.forEach(element => {
this[key].wider_pushUnique(element, settings.array === "unshiftUnique");
});
/* jshint +W083 */
else
this[key].wider_pushUnique(value, settings.array === "unshiftUnique");
break;
case undefined:
case null:
Object.defineProperty(this, key,
Object.getOwnPropertyDescriptor(mergeItems, key));
break;
default:
throw new Error("settings.array='" + settings.array + "' is not a recognised array setting");
}
} //
else if (typeof value === "object") {
if (settings.mode === "overwrite" || this[key] === undefined) {
if (value.wider_deepMerge) {
if (this[key] == null)
this[key] = {};
if (!this[key].wider_deepMerge)
this[key] = {
"<anonymous>": this[key]
};
else if (value.constructor === Object)
this[key].wider_deepMerge(value, settings, path);
else
this[key] = value;
} else
Object.defineProperty(this, key,
Object.getOwnPropertyDescriptor(mergeItems, key));
}
else if (value.constructor === Object)
{
this[key].wider_deepMerge(value, settings, path);
}
} // everything else just gets direct assigned it that were possible
else if (settings.mode === "overwrite")
Object.defineProperty(this, key,
Object.getOwnPropertyDescriptor(mergeItems, key));
// above is clever version of this[key] = value; that preserves the properties
} //
catch (err) {
err.path = valuePath;
err.code = objectPrototype.wider_deepMerge.ERR_CANNOT_UPDATE_AT_PATH;
if (settings.error)
settings.error(err);
else
throw err;
}
}
}
return this;
}
},
/**
* perform an assign of all the [enumerable](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/propertyIsEnumerable) [own properties](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/hasOwnProperty) of the given **object**
*
* Unlike `Object.assign()` this will copy all the defined properties of the submitted items - so eg if your submitted object is writeOnly, it will still be copied leaving a write only result. You should not include the `this`object in the arguments list.
*
* Beware that when assigning functions, getters and setters, that they still remain those functions in their original objects but being called with `this` being the current object into which they are now also inserted.
*
* If a key in the current object is updated, then any defined properties you have already set are also overwritten.
* @function assign()
* @param {...objects} objects any number of objects whose members are to be assigned to this current object
* @returns {Object} being the same as this current object
* @throws {Error} if a key in this current object cannot be updated because the key is already in use and is not updatable
*/
wider_assign: {
enumerable: false,
value: function (...objects) {
for (let index = 0; index < objects.length; index++) {
const object = objects[index];
for (const key in object) {
if (Object.hasOwnProperty.call(object, key)) {
Object.defineProperty(this, key,
Object.getOwnPropertyDescriptor(object, key));
}
}
}
return this;
}
}
};
/**
* @private
* @param {object} target - this will normally be the javascript Function object - but if you dont want to change that object then provide your own equivalent
*/
function assignObject(target = Object) {
/* set the prototype of the javascript Function object or if it already exists then extend it */
methods.wider_deepMerge.ERR_CANNOT_UPDATE_AT_PATH = assignObject.$moduleName + ":ERR_CANNOT_UPDATE_AT_PATH";
Object.defineProperties(target.prototype, methods);
}
assignObject.$moduleName = $moduleName;
if (global.$wider)
global.$wider.registry.register($moduleName, assignObject, "protoObject", "utils");
export default assignObject;