source/utils/ObjectManager.js
import typeDetect from "./typeDetect";
/**
* Utility-class with several static functions to simplify validation, merging and other standard operations on
* javascript-Objects.
*/
export default class ObjectManager {
/**
* Checks that given object is not null and not undefined. Also checks the same inwards in provided nested keys
*
* @example
* let check = this._hasOwnValue({"foo": {"bar": ""}}, false, true, "foo", "bar");
* // check is now false. key "foo" is found, key "bar" is found, but "bar" is emptyString, and params specify to check for them
*
* @example
* let check = this._hasOwnValue({"foo": {"bar": {}}}, true, false, "foo", "bar");
* // check is now false. key "foo" is found, key "bar" is found, but "bar" is {}, and params specify to check for emptyObject.
*
* @example
* let check = this._hasOwnValue({"foo": {"bar": {}}}, false, false, "foo", "bar");
* // check is now true. key "foo" is found, key "bar" is found, so no requested values are null or undefined.
*
* NOTE: Other functions in this file lets you ignore the boolean params - so just use them :)
*
* @param givenObject The object to validate
* @param emptyObject if true - keys mapped to empty object {} will also give false
* @param emptyString if true - keys mapped to empty string "" will also give false
* @param args nested keys to look for, so to validate myObject.foo.bar call this._hasOwnValue(myObject, false, false, "foo". "bar")
* @returns {boolean} true if validation passes, false if not.
*/
static _hasOwnValue(givenObject, emptyObject, emptyString, ...args) {
function checkValue(value) {
return (value != undefined && value != null && (!emptyObject || value != {}) && (!emptyString || value != ""));
}
if (!checkValue(givenObject)) {
return false;
}
for (let key of args) {
if (!(key in givenObject) || !checkValue(givenObject[key])) {
return false;
}
givenObject = givenObject[key];
}
return true;
}
/**
* Validate that an object and nested keys are not null, undefined or empty string "".
*
* @example
* // check that myObject.foo.bar exists:
* validateAllowEmptyObject(myObject, "foo", "bar")
*
* @param givenObject the object to validate
* @param args nested keys to check
* @returns {boolean} true if neither the object or any provided nested key is null, undefined or ""
*/
static validateAllowEmptyObject(givenObject, ...args) {
return this._hasOwnValue(givenObject, false, true, ...args);
}
/**
* Validate that an object and nested keys are not null, undefined or empty object {}.
*
* @example
* // check that myObject.foo.bar exists:
* validateAllowEmptyObject(myObject, "foo", "bar")
*
* @param givenObject the object to validate
* @param args nested keys to check
* @returns {boolean} true if neither the object or any provided nested key is null, undefined or {}
*/
static validateAllowEmptyString(givenObject, ...args) {
return this._hasOwnValue(givenObject, true, false, ...args);
}
/**
* Validate that an object and nested keys are not null, undefined or empty string "" or empty object {}.
*
* @example
* // check that myObject.foo.bar exists:
* validateAllowEmptyObject(myObject, "foo", "bar")
*
* @param givenObject the object to validate
* @param args nested keys to check
* @returns {boolean} true if neither the object or any provided nested key is null, undefined, {} or ""
*/
static validate(givenObject, ...args) {
return this._hasOwnValue(givenObject, true, true, ...args);
}
/**
* Validate that an object and nested keys are not null or undefined.
*
* @example
* // check that myObject.foo.bar exists:
* validateAllowEmptyObject(myObject, "foo", "bar")
*
* @param givenObject the object to validate
* @param args nested keys to check
* @returns {boolean} true if neither the object or any provided nested key is null or undefined.
*/
static validateAllowEmptyStringAndEmptyObject(givenObject, ...args) {
return this._hasOwnValue(givenObject, false, false, ...args);
}
/**
* uses {@link validate} to lookup given args in given objectToBeValidated.
* This ensures the lookup is not null, undefined, empty object, or empty string.
* If this test fails, given fallbackValue is returned.
*
* @example
* // to validate myObject.foo.bar, and get "helloworld" back as default if it is empty:
* validateOrFallback("helloworld", myObject, "foo", "bar")
*
* @param fallbackValue what to return if empty
* @param objectToBeValidated object to do lookup in
* @param args indices used for lookup in object
* @returns {*} lookup in objectToBeValidated if validation succeeded, fallbackValue if not.
*/
static validateOrFallback(fallbackValue, objectToBeValidated, ...args) {
if (!this.validate(objectToBeValidated, ...args)) {
return fallbackValue;
}
for (let arg of args) {
objectToBeValidated = objectToBeValidated[arg];
}
return objectToBeValidated;
}
/**
* Utilityfunction to simplify validation! uses {@link validateOrFallback} for validation, and executes
* given callback (and returns returnvalue from it) if validation fails.
*
* @param callback Function to be executed if validation fails
* @param objectToBeValidated The object to do validation-lookup in
* @param args indices used for lookup in objectToBeValidated
* @returns {*} lookup in objectToBeValidated if validation succeeded, returnvalue from callback if not.
*/
static validateOrCallback(callback, objectToBeValidated, ...args) {
const validatedValue = this.validateOrFallback(null, objectToBeValidated, ...args);
if (validatedValue == null) {
return callback();
}
return validatedValue;
}
/**
* Utilityfunction to simplify validation! uses {@link validateOrCallback} for validation, and passes
* a callback that simply thrown an Error if validation fails.
*
* @param errorMessage the message to use in new Error(errorMessage)
* @param objectToBeValidated the object to validate args in
* @param args args for lookup. see {@link validateOrFallback}
* @returns {*} the looked-up value from objectToBeValidated if it exists
*/
static validateOrError(errorMessage, objectToBeValidated, ...args) {
return this.validateOrCallback(()=>{throw new Error(errorMessage);}, objectToBeValidated, ...args);
}
static _recursiveMerge(mergedValues, overrides) {
for (let key in overrides) {
let detectedType = typeDetect(overrides[key]);
if (detectedType == 'object') {
if(mergedValues[key] == undefined) {
mergedValues[key] = {};
}
mergedValues[key] = this._recursiveMerge(mergedValues[key], overrides[key]);
} else if(detectedType == 'array') {
mergedValues[key] = Array.from(overrides[key]);
} else if(detectedType == 'null' || detectedType == 'number'
|| detectedType == 'boolean' || detectedType == 'string') {
mergedValues[key] = overrides[key];
} else {
throw new Error(`Unsupported type: ${detectedType}.`);
}
}
return mergedValues;
}
/**
* Deep copy all values from overrides to givenObject.
*
* All keys in passed overrides-object will be cloned to passed givenObject. This happens deeply, so all
* nested objects will also be iterated (NOTE: lists are not iterated, only objects).
*
* Note that objects are passed by-reference, so if you do not want givenObject to be modified directly make sure
* you pass false as third param
*
* @param givenObject The object to override values in
* @param overrides The object to copy all values from
* @param overrideValuesInGiven if true givenObjects will be overwritten directly, if false a new object
* will be created to merge both given objects into.
* @returns {*} The result from deep-merging
*/
static _merge(givenObject, overrides, overrideValuesInGiven) {
if (overrideValuesInGiven) {
return this._recursiveMerge(givenObject, overrides);
}
let mergedValues = {};
mergedValues = this._recursiveMerge(mergedValues, givenObject);
return this._recursiveMerge(mergedValues, overrides);
}
/**
* Merges all values from overrideObject into originalObject.
* This happens in place (as objects are passed-by-reference), so originalObject is modified.
*
* This is a deep-merge (unlike Object.assign).
*
* @example <caption>Simple example</caption>
* let originalObject = {
* foo: "bar",
* person: {
* name: "Sandy claws",
* age: 42
* }
* }
*
* let overrideObject = {
* foo: "baz",
* person: {
* age: 23,
* phone: 12345678
* }
* }
*
* ObjectManager.mergeInPlace(originalObject, overrideObject);
*
* // originalObject will now be:
* originalObject == {
* foo: "baz",
* person: {
* age: 23,
* phone: 12345678,
* name: "Sandy claws"
* }
* }
*
* @param originalObject the object to modify
* @param overrideObject the object to copy values from
*/
static mergeInPlace(originalObject, overrideObject) {
this._merge(originalObject, overrideObject, true);
}
/**
* Merges all values from originalObject and overrideObject into a new object that is returned.
*
* This is a deep-merge (unlike Object.assign).
*
* First, all values from originalObject are merged into a new object.
* Then all values from overrideObject are merged into the same object, overriding any corresponding keys from
* originalObject.
*
* @example <caption>Simple example</caption>
* let originalObject = {
* foo: "bar",
* person: {
* name: "Sandy claws",
* age: 42
* }
* }
*
* let overrideObject = {
* foo: "baz",
* person: {
* age: 23,
* phone: 12345678
* }
* }
*
* let mergedObject = ObjectManager.mergeAndCopy(originalObject, overrideObject);
*
* // mergedObject will now be:
* mergedObject == {
* foo: "baz",
* person: {
* age: 23,
* phone: 12345678,
* name: "Sandy claws"
* }
* }
*
* @param originalObject initial values for new object
* @param overrideObject object to override values from original object with
* @returns {{}} new object containing values from originalObject overridden by overrideObject (see example)
*/
static mergeAndClone(originalObject, overrideObject) {
return this._merge(originalObject, overrideObject, false);
}
/**
* Copies all values from given originalObject into a new object, which is returned to caller.
*
* uses {@link ObjectManager#mergeAndClone}, but passes an empty object as one of the two it desires for merging..
*
* @param originalObject
* @returns {{}}
*/
static clone(originalObject) {
return this.mergeAndClone({}, originalObject);
}
}