/**
* Utility functions that help to inspect, enumerate, and create JS objects
* @module lively.lang/object
*/
import { fromString as functionFromString, asScriptOf, argumentNames } from './function.js';
import { deepEquals as arrayDeepEquals, isSubset } from './array.js';
// -=-=-=-=-=-=-=-=-
// internal helper
// -=-=-=-=-=-=-=-=-
// serveral methods in lib/object.js are inspired or derived from
// Prototype JavaScript framework, version 1.6.0_rc1
// (c) 2005-2007 Sam Stephenson
// Prototype is freely distributable under the terms of an MIT-style license.
// For details, see the Prototype web site: http://www.prototypejs.org/
/**
* Returns a stringified representation of an object.
* @param { object } object - The object to generate a stringified representation for.
* @returns { string } The stringified, formatted representation of the object.
*/
function print (object, quote = '\"') {
if (object && Array.isArray(object)) { return '[' + object.map(print) + ']'; }
if (typeof object !== 'string') { return String(object); }
let result = String(object);
result = result.replace(/\n/g, '\\n\\\n');
result = result.replace(/(")/g, '\\$1');
result = result.replace(/(')/g, '\\$1');
result = quote + result + quote;
return result;
}
/**
* Shifts the string a number of times to the right by the contents of `indentString`.
* @param { string } str - The string whose contents to shift.
* @param { string } indentString - The string to insert on the left.
* @param { number } depth - The number of times to indent `str` by.
*/
function indent (str, indentString, depth) {
if (!depth || depth <= 0) return str;
while (depth > 0) { depth--; str = indentString + str; }
return str;
}
const getOwnPropertyDescriptors = (typeof Object.getOwnPropertyDescriptors === 'function')
? Object.getOwnPropertyDescriptors
: function getOwnPropertyDescriptors (object) {
const descriptors = {};
for (const name in object) {
if (!Object.prototype.hasOwnProperty.call(object, name)) continue;
Object.defineProperty(descriptors, name, {
configurable: true,
enumerable: true,
writable: true,
value: Object.getOwnPropertyDescriptor(object, name)
});
}
return descriptors;
};
// -=-=-=-=-
// testing
// -=-=-=-=-
function isArray (obj) { return Array.isArray(obj); }
function isElement (object) { return object && object.nodeType === 1; }
function isFunction (object) { return object instanceof Function; }
function isBoolean (object) { return typeof object === 'boolean'; }
function isString (object) { return typeof object === 'string'; }
function isNumber (object) { return typeof object === 'number'; }
function isUndefined (object) { return typeof object === 'undefined'; }
function isRegExp (object) { return object instanceof RegExp; }
function isObject (object) { return typeof object === 'object'; }
function isPrimitive (obj) {
if (!obj) return true;
switch (typeof obj) {
case 'string':
case 'number':
case 'boolean': return true;
}
return false;
}
function isEmpty (object) {
for (const key in object) { if (object.hasOwnProperty(key)) return false; }
return true;
}
/**
* Helper Method of `equals` below. Idea and partial implementation taken from Chai.JS (MIT, Copyright (c) 2017 Chai.js Assertion Library)
* @param {object} a
* @param {object} b
* @param {array} m - Used for memorizing already compared parts of the objects. Does not need to be set explicitly. Enables comparisons of circular objects.
* @returns {bool} Whether `a` and `b` are equal.
*/
function objectEquals (a, b, m) {
if (m) {
for (let i = 0; i < m.length; i++) {
if ((m[i][0] === a && m[i][1] === b) ||
(m[i][0] === b && m[i][1] === a)) {
return true; // we have seen this pair already, if they are not equal the termination will happen elsewhere
}
}
} else {
m = [];
}
const keysA = Object.keys(a).filter(key => typeof a[key] !== 'function').sort();
const keysB = Object.keys(b).filter(key => typeof b[key] !== 'function').sort();
if (keysA.length !== keysB.length) return false; // different number of keys -> cannot be the same object
// any different key -> cannot be the same objects
for (let i = 0; i < keysA.length; i++) {
if (keysA[i] !== keysB[i]) return false;
}
m.push([a, b]);
// expensive deep comparison
const seenInA = [];
for (let name of keysA) {
seenInA.push(name);
if (!equals(a[name], b[name], m)) return false; // eslint-disable-line no-use-before-define
}
for (let name of keysB) {
if (seenInA.indexOf(name) !== -1) continue; // key existed in A, we already compared successfully above
return false; // we have not seen key in A, thus objects **cannot** be equal
}
return true;
}
/**
* Is object `a` structurally equivalent to object `b`?. Performs a **deep** comparison.
* Functions are completely ignored, with regards to both their implementation and existence/name!
* @param { object } a - The first object to compare.
* @param { object } b - The second object to compare.
* @param { array } m - Used for memorizing already compared parts of the objects. Does not need to be set explicitly. Enables comparisons of circular objects.
* @returns { boolean }
*/
function equals (a, b, m) {
if (a === b) return true;
if (!a || !b) return a === b;
if (Array.isArray(a)) return arrayDeepEquals(a, b, m);
switch (a.constructor) {
case String:
case Date:
case Boolean:
case Number: return a === b;
}
if (typeof a.isEqualNode === 'function') return a.isEqualNode(b);
if (typeof a.equals === 'function') return a.equals(b);
return objectEquals(a, b, m);
}
// -=-=-=-=-=-
// accessing
// -=-=-=-=-=-
const keys = Object.keys;
/**
* Returns the values held by the object properties.
* @example
* var obj1 = {x: 22}, obj2 = {x: 23, y: {z: 3}};
* obj2.__proto__ = obj1;
* obj.values(obj1) // => [22]
* obj.values(obj2) // => [23,{z: 3}]
* @param { object } object - The object to retrive the values from.
* @returns { any[] }
*/
function values (object) {
return object ? Object.keys(object).map(function (k) { return object[k]; }) : [];
}
/**
* Returns a new object that copies all properties with `keys` from `obj`.
* @param { object } obj - The object to collect the properties from.
* @param { string[] } keys - The names of the properties to collect.
* @returns { object }
*/
function select (obj, keys) {
const selected = {};
for (let i = 0; i < keys.length; i++) selected[keys[i]] = obj[keys[i]];
return selected;
}
/**
* Returns a new object that excludes all of the properties defined in `keys`.
* @param { object } object - The object to reduce.
* @param { string[] } keys - The list of properties to exclude.
* @returns { object }
*/
function dissoc (object, keys) {
object = object || {};
const descriptors = getOwnPropertyDescriptors(object);
for (let i = 0; i < keys.length; i++) {
if (keys[i] in descriptors) delete descriptors[keys[i]];
}
return Object.defineProperties({}, descriptors);
}
/**
* Adds a method to a given `object`.
* @param { object } object - The object to extend.
* @param { string|function } funcOrString - The function object or source string for the method.
* @param { string } [optName] - The name of the method.
* @param { object } [optMapping] - The variable mapping for the method, when provided as string.
*/
function addScript (object, funcOrString, optName, optMapping) {
const func = functionFromString(funcOrString);
return asScriptOf(func, object, optName, optMapping);
}
// -=-=-=-=-
// mutation
// -=-=-=-=-
/**
* Add all properties of `source` to `destination`.
* @example
* var dest = {x: 22}, src = {x: 23, y: 24}
* obj.extend(dest, src);
* dest // => {x: 23,y: 24}
* @param { object } destination - The source object.
* @param { object } source - The destination object.
*/
function extend (destination, source) {
let currentCategoryNames = null;
for (let i = 1; i < arguments.length; i++) {
if (typeof arguments[i] === 'string') {
const catName = arguments[i];
if (!destination.categories) destination.categories = {};
if (!destination.categories[catName]) destination.categories[catName] = [];
currentCategoryNames = destination.categories[catName];
continue;
}
source = arguments[i];
for (const property in source) {
const getter = source.__lookupGetter__(property);
const setter = source.__lookupSetter__(property);
if (getter) { destination.__defineGetter__(property, getter); }
if (setter) { destination.__defineSetter__(property, setter); }
if (getter || setter) { continue; }
const sourceObj = source[property];
destination[property] = sourceObj;
if (currentCategoryNames) { currentCategoryNames.push(property); }
if (typeof sourceObj === 'function') {
if (!sourceObj.displayName) { sourceObj.displayName = property; }
// remember the module that contains the definition
if (typeof lively !== 'undefined' &&
lively.Module && lively.Module.current) { sourceObj.sourceModule = lively.Module.current(); }
}
}
}
return destination;
}
// -=-=-=-=-
// clone
// -=-=-=-=-
/**
* Shallow copy.
* @param { object } object - The object to shallow copy.
* @returns { object } The copied object.
*/
function clone (object) {
if (isPrimitive(object)) return object;
if (Array.isArray(object)) return Array.prototype.slice.call(object);
const clone = {};
for (const key in object) {
if (object.hasOwnProperty(key)) { clone[key] = object[key]; }
}
clone.__proto__ = object.__proto__; // ensure same proto
return clone;
}
/**
* Takes a list of properties and returns a new object with those properties shallow-copied from object.
* Similar to `select` but supports an additional `mapFunc`.
* @param { object } object - The object to extract the properties from.
* @param { string[] } properties - The list of properties to extract.
* @param { function } [mapFunc] - Function to map the ectracted properties to custom values.
* @returns { object } A new object with the extracted properties.
*/
function extract (object, properties, mapFunc) {
const copied = {};
for (let i = 0; i < properties.length; i++) {
if (properties[i] in object) {
copied[properties[i]] = mapFunc
? mapFunc(properties[i], object[properties[i]])
: object[properties[i]];
}
}
return copied;
}
// -=-=-=-=-=-
// inspection
// -=-=-=-=-=-
/**
* Prints a human-readable representation of `obj`. The printed
* representation will be syntactically correct JavaScript but will not
* necessarily evaluate to a structurally identical object. `inspect` is
* meant to be used while interactivively exploring JavaScript programs and
* state.
* @param { Object } object - The JavaScript Object to be inspected.
* @param { InspectOptions } options -
* @param { Boolean } options.printFunctionSource - Wether or not to show closures' source code.
* @param { Boolean } options.escapeKeys - Wether or not to escape special characters.
* @param { Number } options.maxDepth - The maximum depth upon which to inspect the object.
* @param { Function } options.customPrinter - Custom print function that returns an alternative string representation of values.
* @param { Number } options.maxNumberOfKeys - Limit the number of keys to be printed of an object.
* @param { Function } options.keySorter - Custom sorting function to define the order in which object key/value pairs are printed.
*/
function inspect (object, options, depth) {
options = options || {};
depth = depth || 0;
if (options.customPrinter) {
const ignoreSignal = options._ignoreSignal || (options._ignoreSignal = {});
const continueInspectFn = (obj) => inspect(obj, options, depth + 1);
const customInspected = options.customPrinter(object, ignoreSignal, continueInspectFn);
if (customInspected !== ignoreSignal) return customInspected;
}
const converter = options.converter || ((key, val) => val);
if (!object) return print(object);
// print function
if (typeof object === 'function') {
return options.printFunctionSource
? String(object)
: 'function' + (object.name ? ' ' + object.name : '') +
'(' + argumentNames(object).join(',') + ') {/*...*/}';
}
// print "primitive"
switch (object.constructor) {
case String:
case Boolean:
case RegExp:
case Number: return print(object, options.quote || '"');
}
if (typeof object.serializeExpr === 'function') { return object.serializeExpr(); }
const isArray = object && Array.isArray(object);
const openBr = isArray ? '[' : '{'; const closeBr = isArray ? ']' : '}';
if (options.maxDepth && depth >= options.maxDepth) { return openBr + '/*...*/' + closeBr; }
let printedProps = [];
if (isArray) {
printedProps = object.map(function (ea) { return inspect(ea, options, depth + 1); });
} else {
let propsToPrint = Object.keys(object)
.sort(function (a, b) {
const aIsFunc = typeof object[a] === 'function';
const bIsFunc = typeof object[b] === 'function';
if (aIsFunc === bIsFunc) {
if (a < b) return -1;
if (a > b) return 1;
return 0;
}
return aIsFunc ? 1 : -1;
});
if (typeof options.keySorter === 'function') {
propsToPrint = propsToPrint.sort(options.keySorter);
}
for (let i = 0; i < propsToPrint.length; i++) {
if (i > (options.maxNumberOfKeys || Infinity)) {
const hiddenEntryCount = propsToPrint.length - i;
printedProps.push(`...${hiddenEntryCount} hidden ${hiddenEntryCount > 1 ? 'entries' : 'entry'}...`);
break;
}
const key = propsToPrint[i];
const isValidLiteral = !key.includes('-');
const printedVal = inspect(converter(key, object[key]), options, depth + 1);
printedProps.push((options.escapeKeys || !isValidLiteral
? JSON.stringify(key)
: key) + ': ' + printedVal);
}
}
if (printedProps.length === 0) { return openBr + closeBr; }
let printedPropsJoined = printedProps.join(', ');
const useNewLines = (!isArray || options.newLineInArrays) &&
(!options.minLengthForNewLine ||
printedPropsJoined.length >= options.minLengthForNewLine);
const ind = indent('', options.indent || ' ', depth);
const propIndent = indent('', options.indent || ' ', depth + 1);
const startBreak = useNewLines && !isArray ? '\n' + propIndent : '';
const eachBreak = useNewLines ? '\n' + propIndent : '';
const endBreak = useNewLines && !isArray ? '\n' + ind : '';
if (useNewLines) printedPropsJoined = printedProps.join(',' + eachBreak);
return openBr + startBreak + printedPropsJoined + endBreak + closeBr;
}
// -=-=-=-=-
// merging
// -=-=-=-=-
/**
* Given a list of objects, return a new object,
* containing all properties of all objects. If the same property exist in
* multiple objects, the right-most property takes precedence.
* Like `extend` but will not mutate objects in `objs`.
* if objs are arrays just concat them
* if objs are real objs then merge properties
* @param { object[] } objs - The list of objects to merge.
*/
function merge (objs) {
if (arguments.length > 1) {
return merge(Array.prototype.slice.call(arguments));
}
if (Array.isArray(objs[0])) { // test for all?
return Array.prototype.concat.apply([], objs);
}
return objs.reduce(function (merged, ea) {
for (const name in ea) {
if (ea.hasOwnProperty(name)) { merged[name] = ea[name]; }
}
return merged;
}, {});
}
/**
* Performs a deep merge of two objects that recursively merges the properties in case
* they are objects.
* @param { object } objA - The first object to merge.
* @param { object } objB - The second object to merge.
*/
function deepMerge (objA, objB) {
if (!objA) return objB;
if (!objB) return objA;
if (Array.isArray(objA)) {
if (!Array.isArray(objB)) return objB;
let merged = objA.map(function (ea, i) { return deepMerge(ea, objB[i]); });
if (objB.length > objA.length) merged = merged.concat(objB.slice(objA.length));
return merged;
}
if (objA.constructor !== Object || objB.constructor !== Object) return objB;
return Object.keys(objA).concat(Object.keys(objB)).reduce(function (merged, name) {
if (!objA[name]) merged[name] = objB[name];
else if (!objB[name]) merged[name] = objA[name];
else if (typeof objA[name] !== 'object' || typeof objB[name] !== 'object') merged[name] = objB[name];
else merged[name] = deepMerge(objA[name], objB[name]);
return merged;
}, {});
}
/**
* Expects `properties` to be a map of keys to objects having optional
* before/after attributes that, if present, should be lists of other property
* keys. `sortProperties` will return an ordered list of property keys so
* that the before / after requirements are fullfilled. If a cyclic
* dependency is encountered an error will be thrown.
* Example:
* ```
* sortProperties({foo: {}, bar: {after: ["foo"], before: ["baz"]}, "baz": {after: ["foo"]}})
* // => ["foo","bar","baz"]
* ```
* ignore-in-doc
* 1. convert "before" requirement into "after" and check if all properties
* mentioned in after/before are actually there
* @param { Map.<string, { after: string, before: string }> } properties - The map of properties to check for.
* @param { boolean } [throwErrorOnMissing=false] - Wether or not to throw an error on detection of missing properties.
*/
function sortKeysWithBeforeAndAfterConstraints (properties, throwErrorOnMissing = false) {
const keys = []; const props = []; const remaining = [];
let stringified = '';
try {
stringified = String(this);
} catch (err) {
}
for (const key in properties) {
const prop = properties[key];
const before = prop.hasOwnProperty('before') ? prop.before : (prop.before = []);
const after = prop.hasOwnProperty('after') ? prop.after : (prop.after = []);
keys.push(key);
props.push(prop);
for (let i = before.length; i--;) {
const beforePropName = before[i];
const beforeProp = properties[beforePropName];
if (!beforeProp) {
console.warn(`[initializeProperties] ${stringified} sortProperties: ` + // eslint-disable-line no-console
`Property ${key} requires to be initialized before ${beforePropName} ` +
'but that property cannot be found.');
before.splice(i, 1);
continue;
}
if (!beforeProp.hasOwnProperty('after')) beforeProp.after = [];
beforeProp.after.push(key);
}
for (let i = after.length; i--;) {
const afterPropName = after[i];
const afterProp = properties[afterPropName];
if (!afterProp) {
console.warn(`[initializeProperties] ${stringified} sortProperties: ` + // eslint-disable-line no-console
`Property ${key} requires to be initialized after ${afterPropName} ` +
'but that property cannot be found.');
after.splice(i, 1);
}
}
remaining.push(key);
}
// compute order
const resolvedGroups = [];
const resolvedKeys = [];
let lastLength = remaining.length + 1;
while (remaining.length) {
if (lastLength === remaining.length) {
throw new Error('Circular dependencies in handler order, could not resolve properties ' +
remaining.map(key => {
const before = properties[key].before; const after = properties[key].after;
if ((!before || !before.length) && (!after || !after.length)) return '';
let report = `${key}\n`;
if (before && before.length) report += ` - before ${before.join(',')}\n`;
if (after && after.length) report += ` - after ${after.join(',')}\n`;
return report;
}).join(''));
}
lastLength = remaining.length;
const resolvedGroup = [];
for (let i = remaining.length; i--;) {
const key = remaining[i];
if (isSubset(properties[key].after, resolvedKeys)) {
remaining.splice(i, 1);
resolvedKeys.push(key);
resolvedGroup.push(key);
}
}
resolvedGroups.push(resolvedGroup);
}
return resolvedGroups.flat();
}
// -=-=-=-=-=-=-
// inheritance
// -=-=-=-=-=-=-
/**
* Wrapper for `Object.create`. Essentially creates a new object that is derived from `obj`;
* @param { object } obj - The object to derive.
* @returns { object } The derived object.
*/
function inherit (obj) { return Object.create(obj); }
/**
* Lookup all properties named name in the proto hierarchy of obj.
* @example
* var a = {foo: 3}, b = Object.create(a), c = Object.create(b);
* c.foo = 4;
* obj.valuesInPropertyHierarchy(c, "foo") // => [3,4]
* @param { object } obj - The object to lookup the property values for.
* @param { string } name - The name of the property to gather the values for.
*/
function valuesInPropertyHierarchy (obj, name) {
const result = []; let lookupObj = obj;
while (lookupObj) {
if (lookupObj.hasOwnProperty(name)) result.unshift(lookupObj[name]);
lookupObj = Object.getPrototypeOf(lookupObj);
}
return result;
}
/**
* like `merge` but automatically gets all definitions of the value in the
* prototype chain and merges those.
* @example
* var o1 = {x: {foo: 23}}, o2 = {x: {foo: 24, bar: 15}}, o3 = {x: {baz: "zork"}};
* o2.__proto__ = o1; o3.__proto__ = o2;
* obj.mergePropertyInHierarchy(o3, "x");
* // => {bar: 15, baz: "zork",foo: 24}
* @param { object } obj - The object to whose property definitions to merge.
* @param { string } propName - The name of the property whose definition to merge.
*/
function mergePropertyInHierarchy (obj, propName) {
return merge(valuesInPropertyHierarchy(obj, propName));
}
/**
* Recursively traverses `object` and its properties to create a copy.
* @param { object } object - The object to copy.
* @returns { object } The deeply copied object.
*/
function deepCopy (object) {
if (!object || typeof object !== 'object' || object instanceof RegExp) return object;
const result = Array.isArray(object) ? Array(object.length) : {};
for (const key in object) {
if (object.hasOwnProperty(key)) { result[key] = deepCopy(object[key]); }
}
return result;
}
// -=-=-=-=-=-=-=-=-
// stringification
// -=-=-=-=-=-=-=-=-
/**
* Returns the constructor's name of a `obj`.
* @param { object } obj
* @returns { string }
*/
function typeStringOf (obj) {
if (obj === null) return 'null';
if (typeof obj === 'undefined') return 'undefined';
return obj.constructor.name;
}
/**
* Returns wether `obj` is a value or mutable type.
* @param { * } obj - The object to check for.
* @returns { boolean }
*/
function isMutableType (obj) {
const immutableTypes = ['null', 'undefined', 'Boolean', 'Number', 'String'];
return immutableTypes.indexOf(typeStringOf(obj)) === -1;
}
/**
* Returns a short stringified representation of `obj`.
* @param { object } obj
* @returns { string }
*/
function shortPrintStringOf (obj) {
// primitive values
if (!isMutableType(obj)) return safeToString(obj); // eslint-disable-line no-use-before-define
// constructed objects
if (obj.constructor.name !== 'Object' && !Array.isArray(obj)) {
if (obj.constructor.name) {
return obj.constructor.name
? obj.constructor.name
: Object.prototype.toString.call(obj).split(' ')[1].split(']')[0];
}
}
// arrays or plain objects
let typeString = '';
function displayTypeAndLength (obj, collectionType, firstBracket, secondBracket) {
if (obj.constructor.name === collectionType) {
typeString += firstBracket;
if (obj.length || Object.keys(obj).length) typeString += '...';
typeString += secondBracket;
}
}
displayTypeAndLength(obj, 'Object', '{', '}');
displayTypeAndLength(obj, 'Array', '[', ']');
return typeString;
}
/**
* Like `toString` but catches errors.
* @param { object } obj - The object the should be converted to string.
* @returns { string }
*/
function safeToString (obj) {
try {
return (obj ? obj.toString() : String(obj)).replace('\n', '');
} catch (e) { return '<error printing object>'; }
}
/**
* Return the object representation if given a primitive value or just the object itself.
* @param { * } obj - The value to convert to object representation if needed.
* @returns { object }
*/
function asObject (obj) {
switch (typeof obj) {
case 'string':
return new String(obj);
case 'boolean':
return new Boolean(obj);
case 'number':
return new Number(obj);
default:
return obj;
}
}
/**
* Returns a name for a key in an object that is not yet occupied.
* @param { object } obj - The object within wich to look for a new unoccupied property name.
* @param { string } [base='_'] - The base name of the property that allows us to generate well formed property names.
* @returns { string } An unoccpuied property name.
*/
function newKeyIn (obj, base = '_') {
let i = 1; let key;
do {
key = base + '-' + i++;
} while (key in obj);
return key;
}
/**
* Convenience method for adjusting the prototype of an object.
*/
const setPrototypeOf = typeof Object.setPrototypeOf === 'function'
? (obj, proto) => Object.setPrototypeOf(obj, proto)
: (obj, proto) => obj.__proto__ = proto;
/**
* Adopts a given object to a new class.
* @param { object } object - The object to change the class for.
* @param { function } newClass - The new class we want to configure for the object.
*/
function adoptObject (object, newClass) {
// change the class of object to newClass
if (newClass === object.constructor) return;
object.constructor = newClass;
setPrototypeOf(object, newClass.prototype);
}
export {
isArray,
isElement,
isFunction,
isBoolean,
isString,
isNumber,
isUndefined,
isRegExp,
isObject,
isPrimitive,
isEmpty,
equals,
keys,
values,
select,
dissoc,
addScript,
extend,
clone,
extract,
inspect,
merge,
deepMerge,
inherit,
valuesInPropertyHierarchy,
mergePropertyInHierarchy,
sortKeysWithBeforeAndAfterConstraints,
deepCopy,
typeStringOf,
shortPrintStringOf,
isMutableType,
safeToString,
asObject,
newKeyIn,
getOwnPropertyDescriptors,
adoptObject,
setPrototypeOf
};