libraries/bugcore/js/src/util/ObjectUtil.js
/*
* Copyright (c) 2016 airbug Inc. http://airbug.com
*
* bugcore may be freely distributed under the MIT license.
*/
//-------------------------------------------------------------------------------
// Annotations
//-------------------------------------------------------------------------------
//@Export('ObjectUtil')
//@Require('FunctionUtil')
//@Require('TypeUtil')
//-------------------------------------------------------------------------------
// Context
//-------------------------------------------------------------------------------
require('bugpack').context("*", function(bugpack) {
//-------------------------------------------------------------------------------
// BugPack
//-------------------------------------------------------------------------------
var FunctionUtil = bugpack.require('FunctionUtil');
var TypeUtil = bugpack.require('TypeUtil');
//-------------------------------------------------------------------------------
// Declare Class
//-------------------------------------------------------------------------------
/**
* @constructor
*/
var ObjectUtil = function() {};
//-------------------------------------------------------------------------------
// Static Private Variables
//-------------------------------------------------------------------------------
/**
* @static
* @private
* @type {boolean}
*/
ObjectUtil.isDontEnumSkipped = true;
// test if properties that shadow DontEnum ones are enumerated
for (var prop in { toString: true }) {
ObjectUtil.isDontEnumSkipped = false;
}
/**
* @static
* @private
* @type {Array}
*/
ObjectUtil.dontEnumProperties = [
'toString',
'toLocaleString',
'valueOf',
'hasOwnProperty',
'isPrototypeOf',
'propertyIsEnumerable',
'constructor'
];
//-------------------------------------------------------------------------------
// Static Methods
//-------------------------------------------------------------------------------
/**
* @static
* @param {Object} into
* @param {...Object} from
* @return {Object}
*/
ObjectUtil.assign = function(into, from) {
var froms = Array.prototype.slice.call(arguments, 1);
if (TypeUtil.isObjectLike(into)) {
froms.forEach(function(from) {
if (TypeUtil.isObjectLike(from)) {
ObjectUtil.forIn(from, function(prop, fromValue) {
ObjectUtil.setProperty(into, prop, fromValue);
});
}
});
return into;
}
throw new Error("into parameter must be Object like");
};
/**
* @static
* @param {Object} object
* @param {string} propertyName
* @param {{
* value: *,
* writable: boolean,
* enumerable: boolean,
* configurable: boolean
* }} description
*/
ObjectUtil.defineProperty = function(object, propertyName, description) {
Object.defineProperty(object, propertyName, description);
};
/**
* @static
* @param {Object} object
* @param {string} propertyQuery
* @param {{
* own: boolean=
* }=} options
* @return {boolean}
*/
ObjectUtil.deleteNestedProperty = function(object, propertyQuery, options) {
if (!TypeUtil.isObjectLike(object)) {
throw new Error("'object' must be an Object");
}
if (!TypeUtil.isString(propertyQuery)) {
throw new Error("'propertyQuery' must be a string");
}
var parts = propertyQuery.split(".");
var propertyValue = object;
for (var i = 0, size = parts.length; i < size; i++) {
var part = parts[i];
if (TypeUtil.isObject(propertyValue)) {
if (ObjectUtil.hasProperty(propertyValue, part, options)) {
if (i === size - 1) {
return ObjectUtil.deleteProperty(propertyValue, part, options);
}
propertyValue = propertyValue[part];
} else {
return false;
}
} else {
return false;
}
}
return false;
};
/**
* @static
* @param {Object} object
* @param {string} propertyName
* @param {{
* own: boolean=
* }=} options
* @return {boolean}
*/
ObjectUtil.deleteProperty = function(object, propertyName, options) {
if (!TypeUtil.isObjectLike(object)) {
throw new TypeError("parameter 'object' must be an object");
}
if (!TypeUtil.isString(propertyName)) {
throw new TypeError("parameter 'propertyName' must be an string");
}
try {
if (ObjectUtil.hasProperty(object, propertyName, options)) {
delete object[propertyName];
return true;
}
} catch(error) {
//do nothing
}
return false;
};
/**
* @static
* @param {Object} object
* @param {function(*, string):*} iteratee
* @param {{
* context: Object=,
* in: boolean=,
* own: boolean=
* }=} options
* @return {Object}
*/
ObjectUtil.for = function(object, iteratee, options) {
return ObjectUtil.iterate(object, iteratee, options);
};
/**
* @static
* @param {Object} object
* @param {function(*, string):*} iteratee
* @param {{
* context: Object=,
* own: boolean=
* }=} options
* @return {Object}
*/
ObjectUtil.forEach = function(object, iteratee, options) {
return ObjectUtil.for(object, iteratee, options);
};
/**
* @static
* @param {Object} object
* @param {function(string, *):*} iteratee
* @param {{
* context: Object=,
* own: boolean=
* }=} options
* @return {Object}
*/
ObjectUtil.forIn = function(object, iteratee, options) {
return ObjectUtil.for(object, iteratee, ObjectUtil.options(options, {in: true}));
};
/**
* @static
* @param {Object} object
* @param {string} propertyQuery
* @param {{
* own: boolean=
* }=} options
* @return {boolean}
*/
ObjectUtil.hasNestedProperty = function(object, propertyQuery, options) {
if (!TypeUtil.isObjectLike(object)) {
throw new Error("'object' must be an Object");
}
if (!TypeUtil.isString(propertyQuery)) {
throw new Error("'propertyQuery' must be a string");
}
var parts = propertyQuery.split(".");
var propertyValue = object;
for (var i = 0, size = parts.length; i < size; i++) {
var part = parts[i];
if (TypeUtil.isObject(propertyValue)) {
if (ObjectUtil.hasProperty(propertyValue, part, options)) {
propertyValue = propertyValue[part];
} else {
return false;
}
} else {
return false;
}
}
return true;
};
/**
* @static
* @param {Object} object
* @param {string} propertyQuery
* @param {{
* own: boolean=
* }=} options
* @return {*}
*/
ObjectUtil.getNestedProperty = function(object, propertyQuery, options) {
// NOTE BRN: We're trying to dig down in to the property object. So if we have a property Object like this
// {
// name: {
// subName: "someValue"
// }
// }
// and we request a property named "name.subName", then "someValue" should be returned. If we request the property
// "name" then the object {subName: "someValue"} will be returned.
var parts = propertyQuery.split(".");
var propertyValue = object;
for (var i = 0, size = parts.length; i < size; i++) {
var part = parts[i];
if (TypeUtil.isObject(propertyValue) && ObjectUtil.hasProperty(propertyValue, part, options)) {
propertyValue = propertyValue[part];
} else {
return undefined;
}
}
return propertyValue;
};
/**
* @static
* @param {Object} object
* @param {{
* own: boolean=
* }=} options
* @return {Array.<string>}
*/
ObjectUtil.getProperties = function(object, options) {
var propertyArray = [];
ObjectUtil.forIn(object, function(propertyName) {
propertyArray.push(propertyName);
}, null, options);
return propertyArray;
};
/**
* @static
* @param {Object} object
* @param {string} propertyName
* @param {{
* own: boolean=
* }=} options
* @return {*}
*/
ObjectUtil.getProperty = function(object, propertyName, options) {
if (ObjectUtil.hasProperty(object, propertyName, options)) {
return object[propertyName];
}
return undefined;
};
/**
* @static
* @param {Object} object
* @param {string} propertyName
* @param {{
* own: boolean=
* }=} options
*/
ObjectUtil.hasProperty = function(object, propertyName, options) {
if (!TypeUtil.isObjectLike(object)) {
throw new TypeError("'object' must be an Object");
}
if (TypeUtil.isObject(options) && options.own) {
return Object.prototype.hasOwnProperty.call(object, propertyName);
}
return (propertyName in object);
};
/**
* @static
* @param {Object} object
* @return {boolean}
*/
ObjectUtil.isEmpty = function(object) {
return ObjectUtil.getProperties(object).length === 0;
};
/**
* @static
* @param {Object} object1
* @param {Object} object2
* @param {boolean=} deep
* @return {boolean}
*/
ObjectUtil.isEqual = function(object1, object2, deep) {
//TODO BRN: Implement deep parameter
if (!TypeUtil.isObject(object1)) {
throw new TypeError( "'object1' must be an Object");
}
if (!TypeUtil.isObject(object2)) {
throw new TypeError( "'object2' must be an Object");
}
if (object1 === object2) {
return true;
}
var object1Properties = ObjectUtil.getProperties(object1);
var object2Properties = ObjectUtil.getProperties(object2);
if (object1Properties.length !== object2Properties.length) {
return false;
}
for (var i = 0, size = object1Properties.length; i < size; i++) {
if (object1Properties[i] !== object2Properties[i]) {
return false;
}
if (object1[object1Properties[i]] !== object2[object2Properties[i]]) {
return false;
}
}
return true;
};
/**
* @license MIT License
* This work is based on the code found here
* https://github.com/kangax/protolicious/blob/master/experimental/object.for_in.js#L18
*
* NOTE BRN: If a property is modified in one iteration and then visited at a later time, its value in the loop is
* its value at that later time. A property that is deleted before it has been visited will not be visited later.
* Properties added to the object over which iteration is occurring may either be visited or omitted from iteration.
* In general it is best not to add, modify or remove properties from the object during iteration, other than the
* property currently being visited. There is no guarantee whether or not an added property will be visited, whether
* a modified property (other than the current one) will be visited before or after it is modified, or whether a
* deleted property will be visited before it is deleted.
*
* @static
* @param {Object} object
* @param {function(*, string):*} iteratee
* @param {{
* context: Object=,
* in: boolean=,
* own: boolean=
* }=} options
* @return {Object}
*/
ObjectUtil.iterate = function(object, iteratee, options) {
if (!TypeUtil.isObjectLike(object)) {
throw new TypeError("'object' must be Object like");
}
if (!iteratee || (iteratee && !iteratee.call)) {
throw new Error('Iterator function is required');
}
options = ObjectUtil.options(options);
var context = options.context;
var _in = options.in;
for (var propertyName in object) {
if (ObjectUtil.hasProperty(object, propertyName, options)) {
var args = _in ? [propertyName, object[propertyName]] : [object[propertyName], propertyName];
var result = FunctionUtil.apply(iteratee, context, args.concat(object));
if (result === false) {
break;
}
}
}
if (ObjectUtil.isDontEnumSkipped) {
for (var i = 0, size = ObjectUtil.dontEnumProperties.length; i < size; i++) {
var dontEnumPropertyName = ObjectUtil.dontEnumProperties[i];
if (ObjectUtil.hasProperty(object, dontEnumPropertyName, options)) {
var args = _in ? [dontEnumPropertyName, object[dontEnumPropertyName]] : [object[dontEnumPropertyName], dontEnumPropertyName];
var result = FunctionUtil.call(iteratee, context, args.concat(object));
if (result === false) {
break;
}
}
}
}
return object;
};
/**
* @static
* @param {Object} into
* @param {...Object} from
* @return {Object}
*/
ObjectUtil.merge = function(into, from) {
var froms = Array.prototype.slice.call(arguments, 1);
if (TypeUtil.isObjectLike(into)) {
froms.forEach(function(from) {
if (TypeUtil.isObjectLike(from)) {
ObjectUtil.forIn(from, function(prop, fromValue) {
var intoValue = into[prop];
if (TypeUtil.isObjectLike(fromValue) && TypeUtil.isObjectLike(intoValue)) {
ObjectUtil.merge(intoValue, fromValue);
} else {
ObjectUtil.setProperty(into, prop, fromValue);
}
});
} else {
throw new Error("from parameter must be Object like");
}
});
return into;
}
throw new Error("into parameter must be Object like");
};
/**
* @static
* @param {Object} object
* @param {...(Array.<string> | string)} properties
* @return {Object}
*/
ObjectUtil.omit = function(object, properties) {
if (!TypeUtil.isObjectLike(object)) {
throw new TypeError("parameter 'object' must be an Object");
}
var args = Array.prototype.slice.call(arguments, 1);
var omitProperties = [];
args.forEach(function(arg) {
if (TypeUtil.isArray(arg)) {
omitProperties = omitProperties.concat(arg);
} else if (TypeUtil.isString(arg)) {
omitProperties = omitProperties.concat([arg]);
} else {
throw new TypeError("parameter 'properties' must be an Array or a string");
}
});
var omitted = {};
ObjectUtil.forIn(object, function(property, value) {
if (omitProperties.indexOf(property) === -1) {
omitted[property] = value;
}
});
return omitted;
};
/**
* @static
* @param {Object} object
* @param {...(Array.<string> | string)} properties
* @return {Object}
*/
ObjectUtil.pick = function(object, properties) {
if (!TypeUtil.isObjectLike(object)) {
throw new TypeError("parameter 'object' must be an Object");
}
var args = Array.prototype.slice.call(arguments, 1);
var pickProperties = [];
args.forEach(function(arg) {
if (TypeUtil.isArray(arg)) {
pickProperties = pickProperties.concat(arg);
} else if (TypeUtil.isString(arg)) {
pickProperties = pickProperties.concat([arg]);
} else {
throw new TypeError("parameter 'properties' must be an Array or a string");
}
});
var picked = {};
pickProperties.forEach(function(property) {
if (ObjectUtil.hasProperty(object, property)) {
picked[property] = object[property];
}
});
return picked;
};
/**
* @static
* @param {Object} object
* @param {string} propertyQuery
* @param {*} value
* @return {boolean}
*/
ObjectUtil.setNestedProperty = function(object, propertyQuery, value) {
if (!TypeUtil.isObjectLike(object)) {
throw new TypeError("parameter 'object' must be an object");
}
if (!TypeUtil.isString(propertyQuery)) {
throw new TypeError("parameter 'propertyQuery' must be an string");
}
var parts = propertyQuery.split(".");
var propertyValue = object;
for (var i = 0, size = parts.length; i < size; i++) {
var part = parts[i];
if (i === size - 1) {
return ObjectUtil.setProperty(propertyValue, part, value);
} else {
if (!TypeUtil.isObject(propertyValue[part])) {
ObjectUtil.setProperty(propertyValue, part, {});
}
propertyValue = propertyValue[part];
}
}
};
/**
* @static
* @param {Object} object
* @param {string} propertyName
* @param {*} value
* @return {boolean}
*/
ObjectUtil.setProperty = function(object, propertyName, value) {
if (!TypeUtil.isObjectLike(object)) {
throw new TypeError("parameter 'object' must be an object");
}
if (!TypeUtil.isString(propertyName)) {
throw new TypeError("parameter 'propertyName' must be an string");
}
try {
object[propertyName] = value;
return true;
} catch(error) {
return false;
}
};
/**
* @static
* @param {Object} object
* @return {string}
*/
ObjectUtil.toConstructorName = function(object) {
if (!TypeUtil.isObjectLike(object)) {
throw new TypeError("parameter 'object' must be an object");
}
return FunctionUtil.toName(object.constructor);
};
//-------------------------------------------------------------------------------
// Static Private Methods
//-------------------------------------------------------------------------------
/**
* @static
* @private
* @param {Object=} options
* @param {Object=} overrides
* @returns {Object}
*/
ObjectUtil.options = function(options, overrides) {
options = options || {};
for (var key in overrides) {
options[key] = overrides[key];
}
return options;
};
//-------------------------------------------------------------------------------
// Exports
//-------------------------------------------------------------------------------
bugpack.export('ObjectUtil', ObjectUtil);
});