diff --git a/object.js b/object.js index bd11e03..4d32e21 100755 --- a/object.js +++ b/object.js @@ -1,1145 +1,1205 @@ -/********************************************************************** -* -* object.js -* -* This is a set of tools and abstractions to create and manage -* constructors, objects and prototype chains in idiomatic JavaScript. -* -* Motivation: -* This package was originally written to unify low level object -* definitios within a large project and from there evolved to be a -* full functional alternative to the ES6 class notation with all of -* its inconsistencies, hoops, "the same but slightly different" ways -* to do things and "magic" (hidden) functionality. -* -* Repo and docs: -* https://github.com/flynx/object.js -* -* -***********************************************/ /* c8 ignore next 2 */ -((typeof define)[0]=='u'?function(f){module.exports=f(require)}:define) -(function(require){ var module={} // make module AMD/node compatible... -/*********************************************************************/ - - -// Function methods to link into a constructor producing a callable -// defined via .__call__(..) -// -// These are needed to support the expected popular function API in a -// callable potentially not related to a function. -// -// see: Constructor(..) for details. -module.LINK_FUNCTION_METHODS = [ - 'call', - 'apply', - 'bind', -] - - - - -//--------------------------------------------------------------------- -// Helpers... - -module.TAB_SIZE = 4 - -module.LEADING_TABS = 1 - - -// Normalize code indent... -// -// normalizeIndent(text) -// -> text -// -// -// This will remove common indent from each line of text, this is useful -// for printing function code of functions that were defined at deep -// levels of indent. -// -// This will ignore the indent of the first line. -// -// If the last line is indented higher or equal to the rest of the text -// we will use leading_tabs (defaults to LEADING_TABS) to indent the -// rest of the text. -// This will indent the following styles correctnly: -// -// |function(a, b){ |function(a, b){ -// | return a + b } | return a + b -// | |} -// -// -// NOTE: this will trim out both leading and trailing white-space. -// NOTE: this is generally code-agnostic with one sigificant -// exception -- normalizeIndent(..) will break code written -// in Whitespace. -// -// XXX is this the right place for this??? -// ...when moving take care that ImageGrid's core.doc uses this... -var normalizeIndent = -module.normalizeIndent = -function(text, tab_size, leading_tabs){ - tab_size = tab_size == null ? - module.TAB_SIZE - : tab_size - leading_tabs = (leading_tabs == null ? - module.LEADING_TABS - : leading_tabs) - * tab_size - // prepare text... - var tab = ' '.repeat(tab_size || 0) - text = tab != '' ? - text.replace(/\t/g, tab) - : text - // trim the tail and remove leading blank lines... - var lines = text.trimEnd().split(/\n/) - while(lines.length > 0 - && lines[0].trim() == ''){ - // XXX we have two options here: - // - indent everyline including the first non-blank - // - do not indent anything (current) - // ...not sure which is best... - leading_tabs = 0 - lines.shift() } - // count common indent... - var l = lines - .reduce(function(l, e, i){ - var indent = e.length - e.trimLeft().length - return e.trim().length == 0 - // ignore 0 indent of first line... - || (i == 0 && indent == 0) ? - l - // last line... - : i == lines.length-1 - && indent >= l ? - // XXX feels a bit overcomplicated... - (l < 0 ? - // last of two with 0 indent on first -> indent... - Math.max(indent - leading_tabs, 0) - // ignore leading_tabs if lower indent... - : Math.min(l, Math.max(indent - leading_tabs, 0))) - // initial state... - : l < 0 ? - indent - // min... - : Math.min(l, indent) }, -1) || 0 - // normalize... - return lines - .map(function(line, i){ - return i == 0 ? - line - : line.slice(l) }) - .join('\n') - .trim() } - - -// shorthand more suted for text... -var normalizeTextIndent = -module.normalizeTextIndent = -function(text, tab_size, leading_tabs){ - return module.normalizeIndent(text, tab_size, leading_tabs || 0) } - - -// template string tag versions of the above... -var doc = -module.doc = -function(strings, ...values){ - return normalizeIndent(strings - .map(function(s, i){ return s + (values[i] || '') }) - .join('')) } - -var text = -module.text = -function(strings, ...values){ - return normalizeTextIndent(strings - .map(function(s, i){ return s + (values[i] || '') }) - .join('')) } - - -// Get keys from prototype chain... -// -// deepKeys(obj) -// deepKeys(obj, stop) -// -> keys -// -// -// NOTE: this is like Object.keys(..) but will get keys for all levels -// till stop if given... -// -// XXX should we add this to Object??? -var deepKeys = -module.deepKeys = -function(obj, stop){ - var res = [] - while(obj != null){ - res.push(Object.keys(obj)) - if(obj === stop){ - break } - obj = obj.__proto__ } - return [...(new Set(res.flat()))] } - - -// Match two objects... -// -// match(a, b) -// -> bool -// -// -// This will match objects iff: -// - if they are identical or -// - attr count is the same and, -// - attr names are the same and, -// - attr values are identical. -// -// -// Non-strict match... -// match(a, b, true) -// -// This is similar to the default case but uses equality rather than -// identity to match values. -// -// -// NOTE: this will do a shallow test using Object.keys(..) thus .__proto__ -// attributes are ignored... -var match = -module.match = -function(base, obj, non_strict){ - // identity... - if(base === obj){ - return true } - // typeof -- sanity check... - if(typeof(base) != typeof(obj)){ - return false } - // attr count... - var o = Object.keys(Object.getOwnPropertyDescriptors(obj)) - if(Object.keys(Object.getOwnPropertyDescriptors(base)).length != o.length){ - return false } - // names and values... - o = o.map(function(k){ - return [k, obj[k]] }) - while(o.length > 0){ - var [k, v] = o.pop() - if(!base.hasOwnProperty(k) - || (non_strict ? - base[k] != v - : base[k] !== v)){ - return false } } - return true } - - -// Like .match(..) but will test if obj is a non-strict subset of base... -// -// NOTE: this will only check direct attributes of both base and obj. -var matchPartial = -module.matchPartial = -function(base, obj, non_strict){ - return base === obj - || Object.entries(obj) - .filter(function([n, v]){ - return !base.hasOwnProperty(n) - || (non_strict ? - base[n] != v - : base[n] !== v) }) - .length == 0 } - - - -//--------------------------------------------------------------------- -// Prototype chain content access... - -// object to trigger iteration stop... -// -// NOTE: this is a placeholder for documnetation/context purposes, see -// the actual implementation at the end of the module... -module.STOP = undefined - - -// Get a list of source objects for a prop/attr name... -// -// sources(obj, name) -// sources(obj, name, callback) -// -> list -// -> [] -// -// Get callables or objects defining .__call__ (special-case) -// sources(obj, '__call__') -// sources(obj, '__call__', callback) -// -> list -// -> [] -// -// Get full chain... -// sources(obj) -// sources(obj, callback) -// -> list -// -// -// callback(obj) -// -> STOP -// -> STOP(value) -// -> .. -// -// -// The callback(..) is called with each matching object. -// -// callback(..) return values: -// - STOP - stop the search and return the match list terminated -// with the object triggering the stop. -// - STOP(value) - stop the search and return the match list terminated -// with the value passed to STOP(..) -// - undefined - return the triggering object as-is -// NOTE: this is the same as returning [obj] -// - array - merge array content into the result insteaad of -// the triggering value. -// NOTE: an ampty array will effectively omit the -// triggering object from the results. -// - other - return a value instead of the triggering object. -// -// -// NOTE: this gos up the prototype chain, not caring about any role ( -// instance/class or instance/prototype) bounderies and depends -// only on the object given as the starting point. -// It is possible to start the search from this, thus checking -// for any overloading in the instance, though this approach is -// not very reusable.... -// NOTE: this will not trigger any props... -var sources = -module.sources = -function(obj, name, callback){ - // get full chain... - if(typeof(name) == 'function'){ - callback = name - name = undefined - } - var o - var res = [] - while(obj != null){ - //if(obj.hasOwnProperty(name)){ - if(name === undefined - || obj.hasOwnProperty(name) - || (name == '__call__' && typeof(obj) == 'function')){ - // handle callback... - o = callback - && callback(obj) - // manage results... - res.push( - (o === undefined || o === module.STOP) ? - [obj] - : o instanceof module.STOP ? - o.value - : o ) - // stop... - if(o === module.STOP - || o instanceof module.STOP){ - return res.flat() } } - obj = obj.__proto__ } - return res.flat() } - - -// Get a list of values/props set in source objects for a prop/attr name... -// -// Get values... -// values(obj, name) -// values(obj, name, callback) -// -> list -// -> [] -// -// Get propery descriptors... -// values(obj, name, true) -// values(obj, name, callback, true) -// -> list -// -> [] -// -// callback(value/prop, obj) -// -> STOP -// -> STOP(value) -// -> .. -// -// -// Special case: name is given as '__call__' -// This will return either the value the object if it is callable -// or the value of .__call__ attribute... -// -// -// NOTE: for more docs on the callback(..) see sources(..) -var values = -module.values = -function(obj, name, callback, props){ - props = callback === true ? - callback - : props - var _get = function(obj, name){ - return props ? - Object.getOwnPropertyDescriptor(obj, name) - // handle callable instance... - : !(name in obj) - && name == '__call__' - && typeof(obj) == 'function' ? - obj - // normal attr... - : obj[name] } - // wrap the callback if given... - var c = typeof(callback) == 'function' - && function(obj){ - var val = _get(obj, name) - var res = callback(val, obj) - return res === module.STOP ? - // wrap the expected stop result if the user did not do it... - module.STOP(val) - : res } - return c ? - // NOTE: we do not need to handle the callback return values as - // this is fully done by c(..) in sources(..) - sources(obj, name, c) - : sources(obj, name) - .map(function(obj){ - return _get(obj, name) }) } - - -// Find the next parent attribute in the prototype chain. -// -// Get parent attribute value... -// parent(proto, name) -// -> value -// -> undefined -// -// Get parent callable or .__call__ value (special-case) -// parent(proto, '__call__') -// -> value -// -> undefined -// -// Get parent method... -// parent(method, this) -// -> meth -// -> undefined -// -// Get parent object... -// parent(this) -// -> parent -// -// -// The two forms differ in: -// - in parent(method, ..) a method's .name attr is used for name. -// - in parent(method, ..) the containing prototype is inferred. -// -// NOTE: there are cases where method.name is not set (e.g. anonymous -// function), so there a name should be passed explicitly... -// NOTE: when passing a method it is recommended to pass an explicit -// reference to it relative to the constructor, i.e.: -// Constructor.prototype.method -// this will avoid relative resolution loops, for example: -// this.method -// deep in a chain will resolve to the first .method value visible -// from 'this', i.e. the top most value and not the value visible -// from that particular level... -// -// -// Example: -// var X = object.Constructor('X', { -// __proto__: Y.prototype, -// -// attr: 123, -// -// method: function(){ -// // get attribute... -// var a = object.parent(X.prototype, 'attr') -// -// // get method... -// var ret = object.parent(X.prototype.method, this) -// .call(this, ...arguments) -// -// // ... -// } -// }) -// -// -// NOTE: in the general case this will get the value of the returned -// property/attribute, the rest of the way passive to props. -// The method case will get the value of every method from 'this' -// and to the method after the match. -// NOTE: this is super(..) replacement, usable in any context without -// restriction -- super(..) is restricted to class methods only... -var parent = -module.parent = -function(proto, name){ - // special case: get parent... - if(arguments.length == 1){ - return proto.__proto__ } - // special case: get method... - if(typeof(name) != typeof('str')){ - var that = name - name = proto.name - // sanity check... - if(name == ''){ - throw new Error('parent(..): need a method with non-empty .name') } - // get first matching source... - proto = sources(that, name, - function(obj){ - return obj[name] === proto - && module.STOP }) - .pop() } - // get first source... - var c = 0 - var res = sources(proto, name, - function(obj){ - return c++ == 1 - && module.STOP }) - .pop() - return !res ? - undefined - :(!(name in res) && typeof(res) == 'function') ? - res - : res[name] } - - -// Find the next parent property descriptor in the prototype chain... -// -// parentProperty(proto, name) -// -> prop-descriptor -// -// -// This is like parent(..) but will get a property descriptor... -var parentProperty = -module.parentProperty = -function(proto, name){ - // get second source... - var c = 0 - var res = sources(proto, name, - function(obj){ - return c++ == 1 - && module.STOP }) - .pop() - return res ? - // get next value... - Object.getOwnPropertyDescriptor(res, name) - : undefined } - - -// Find the next parent method and call it... -// -// parentCall(proto, name, this, ..) -// parentCall(meth, this, ..) -// -> res -// -> undefined -// -// -// This also gracefully handles the case when no higher level definition -// is found, i.e. the corresponding parent(..) call will return undefined -// or a non-callable. -// -// NOTE: this is just like parent(..) but will call the retrieved method, -// essentially this is a shorthand to: -// parent(proto, name).call(this, ...) -// or: -// parent(method, this).call(this, ...) -// NOTE: for more docs see parent(..) -var parentCall = -module.parentCall = -function(proto, name, that, ...args){ - var meth = parent(proto, name) - return typeof(meth) == 'function' ? - meth.call(...( typeof(name) == typeof('str') ? - [...arguments].slice(2) - : [...arguments].slice(1) )) - : undefined } - - -// Test if child is related to parent... -// -// parentOf(parent, child) -// -> bool -// -// -// NOTE: this is like a instanceof b but within the prototype chain -var parentOf = -module.parentOf = -function(parent, child){ - return new Set(sources(child)).has(parent) } - -// Reverse of parentOf(..) -var childOf = -module.childOf = -function(child, parent){ - return parentOf(parent, child) } - -var related = -module.related = -function(a, b){ - return parentOf(a, b) - || parentOf(b, a) } - - - -//--------------------------------------------------------------------- -// Mixin utils... - -// Mix a set of methods/props/attrs into an object... -// -// mixinFlat(base, object, ...) -// -> base -// -// -// NOTE: essentially this is just like Object.assign(..) but copies -// properties directly rather than copying property values... -// NOTE: this will not transfer several the special variables not listed -// by Object.keys(..). -// This includes things like .__proto__ -// NOTE: this can and will overwrite attributes... -var mixinFlat = -module.mixinFlat = -function(base, ...objects){ - return objects - .reduce(function(base, cur){ - Object.keys(cur) - .map(function(k){ - Object.defineProperty(base, k, - Object.getOwnPropertyDescriptor(cur, k)) }) - return base }, base) } - - -// Mix sets of methods/props/attrs into an object as prototypes... -// -// mixin(base, object, ..) -// -> base -// -// -// This will create a new object per set of methods given and -// mixinFlat(..) the method set into this object leaving the -// original objects intact. -// -// base <-- object1_copy <-- .. <-- objectN_copy <- base.__proto__ -// -// -// NOTE: this will only mix in non-empty objects... -// NOTE: mixing into a constructor will break object creation via new... -// Example: -// class A {} -// class B extends A {} -// -// mixin(B, {x: 123}) -// -// var b = new B() // will break... -// -// This does not affect object.Constructor(..) chains... -// NOTE: mixin(Object.prototype, ..) will fail because Object.prototype.__proto__ -// is imutable... -var mixin = -module.mixin = -function(base, ...objects){ - base.__proto__ = objects - .reduce(function(res, cur){ - return Object.keys(cur).length > 0 ? - module.mixinFlat(Object.create(res), cur) - : res }, base.__proto__) - return base } - - -// Get matching mixins... -// -// mixins(base, object[, callback]) -// mixins(base, list[, callback]) -// -> list -// -// -// callback(base, obj, parent) -// -> STOP -// -> undefined -// -// -// NOTE: this will also match base... -// NOTE: if base matches directly callback(..) will get undefined as parent -// NOTE: for more docs on the callback(..) see sources(..) -var mixins = -module.mixins = -function(base, object, callback){ - object = object instanceof Array ? - object - : [object] - var res = [] - var o - var parent - while(base != null){ - // match each object... - for(var obj of object){ - if(match(base, obj)){ - o = callback - && callback(base, obj, parent) - // manage results... - res.push( - (o === undefined || o === module.STOP) ? - [base] - : o instanceof module.STOP ? - o.value - : o ) - if(o === module.STOP - || o instanceof module.STOP){ - return res.flat() } - // match found, no need to test further... - break } } - parent = base - base = base.__proto__ } - return res.flat() } - - -// Check of base has mixin... -// -// hasMixin(base, mixin) -// -> bool -// -// -// NOTE: to test for a flat mixin directly use .matchPartial(base, object) -var hasMixin = -module.hasMixin = -function(base, object){ - return ( - // normal mixin... - mixins(base, object, function(){ return module.STOP }) - .length > 0 - // flat mixin search... - || sources(base, function(p){ - return matchPartial(p, object) ? - module.STOP - : [] }) - .length > 0 )} - - -// Mix-out sets of methods/props/attrs out of an object prototype chain... -// -// Mix-out first occurrence of each matching object... -// mixout(base, object, ..) -// mixout(base, 'first', object, ..) -// -> base -// -// Mix-out all occurrences of each matching object... -// mixout(base, 'all', object, ..) -// -> base -// -// -// NOTE: this is the opposite to mixin(..) -// NOTE: this used mixins(..) / match(..) to find the relevant mixins, -// see those for more info... -var mixout = -module.mixout = -function(base, ...objects){ - var all = objects[0] == 'all' ? - !!objects.shift() - : objects[0] == 'first' ? - !objects.shift() - : false - var remove = [] - mixins(base, objects, function(match, obj, parent){ - parent && remove.push(parent) - // when removing the first occurrence, don't check for obj again... - all || objects.splice(objects.indexOf(obj), 1) }) - // NOTE: we are removing on a separate stage so as not to mess with - // mixins(..) iterating... - remove - // XXX not sure why this is needed, needs thought... - .reverse() - .forEach(function(p){ - p.__proto__ = p.__proto__.__proto__ }) - return base } - - - -//--------------------------------------------------------------------- -// Constructor... - -// Make an uninitialized instance object... -// -// RawInstance(context, constructor, ...) -// -> instance -// -// -// This will: -// - construct an object -// - if .__new__(..) is defined -// -> call and use its return value -// - if prototype is a function or if .__call__(..) is defined -// -> use a wrapper function -// - if construct.__proto__ has .__rawinstance__(..) -// -> use it to create an instance -// - if constructor.__proto__ is a constructor -// -> use it to create an instance -// - else -// -> use {} -// - link the object into the prototype chain -// -// -// This will not call .__init__(..), hence the "uninitialized". -// -// -// NOTE: "context" is only used when passeding to .__new__(..) if defined, -// and is ignored otherwise... -// NOTE: as this is simply an extension to the base JavaScript protocol this -// can be used to construct any object... -// Example: -// // new is optional... -// var l = new RawInstance(null, Array, 'a', 'b', 'c') -// NOTE: the following are not the same in structure but functionally -// are identical: -// var C = Constructor('C', function(){ .. }) -// and -// var C2 = Constructor('C2', { __call__: function(){ .. } }) -// the difference is in C.prototype vs. C2.prototype, the first -// being a function while the second is an object with a call -// method... -// NOTE: essentially this is an extended version of Reflect.construct(..) -var RawInstance = -module.RawInstance = -function(context, constructor, ...args){ - var _mirror_doc = function(func, target){ - Object.defineProperty(func, 'toString', { - value: function(...args){ - var f = typeof(target.prototype) == 'function' ? - target.prototype - : target.prototype.__call__ - return typeof(f) == 'function' ? - module.normalizeIndent(f.toString(...args)) - : undefined }, - enumerable: false, - }) - return func } - - var obj = - // prototype defines .__new__(..)... - constructor.prototype.__new__ instanceof Function ? - constructor.prototype.__new__(context, ...args) - // native constructor... - : /\[native code\]/.test(constructor.toString()) ? - Reflect.construct(constructor, args) - // callable instance... - // NOTE: we need to isolate the callable from instances, thus we - // reference 'constructor' directly rather than using - // 'this.constructor'... - : (typeof(constructor.prototype) == 'function' - || constructor.prototype.__call__ instanceof Function) ? - _mirror_doc( - function(){ - return ( - // .prototype is a function... - typeof(constructor.prototype) == 'function' ? - // NOTE: we are not using .call(..) here as it - // may not be accesible through the prototype - // chain, this can occur when creating a - // callable instance from a non-callable - // parent... - Reflect.apply( - constructor.prototype, obj, [this, ...arguments]) - // .__call__(..) or fail semi-gracefully... - : constructor.prototype.__call__ - .call(obj, this, ...arguments)) }, - constructor) - // recursively call .__rawinstance__(..) - : constructor.__proto__.__rawinstance__ ? - constructor.__proto__.__rawinstance__(context, ...args) - // use parent's constructor... - : (typeof(constructor.__proto__) == 'function' - // XXX for some reason if using (function(){}).__proto__ - // instead of Function.prototype below coverage is - // not counted for the condition.... - && constructor.__proto__ !== Function.prototype) ? - Reflect.construct(constructor.__proto__, args, constructor) - // default object base... - : Reflect.construct(Object, [], constructor) - - // link to prototype chain, if not done already... - obj.__proto__ !== constructor.prototype - && (obj.__proto__ = constructor.prototype) - - return obj } - - -// Make an object constructor function... -// -// Make a constructor with an object prototype... -// Constructor(name, proto) -// -> constructor -// -// Make a constructor with a prototype and a constructor prototype... -// Constructor(name, constructor-mixin, proto) -// -> constructor -// -// Make a constructor with prototype extending parent-constructor... -// Constructor(name, parent-constructor, proto) -// Constructor(name, parent-constructor, constructor-mixin, proto) -// -> constructor -// -// -// The resulting constructor can produce objects in one of these ways: -// -// Create instance... -// constructor(..) -// new constructor -// new constructor(..) -// -> instance -// -// Create raw/uninitialized instance... -// constructor.__rawinstance__(..) -// RawInstance(null, constructor, ..) -// -> raw-instance -// -// -// All produced objects are instances of the constructor -// instance instanceof constructor -// -> true -// -// -// -// Create and initialization protocol: -// 1) raw instance is created: -// a) constructor.__rawinstance__(..) / RawInstance(..) called: -// - call .__new__(..) if defined and get return value as -// instance, or -// - if .__call__(..) defined or prototype is a function, wrap -// it and use the wrapper function as instance, or -// - create an empty object -// b) instance linked to prototype chain -// set .__proto__ to constructor.prototype -// 2) instance is initialized: -// call .__init__(..) if defined -// -// -// Special attributes: -// -// Sets parent constructor -// .__extends__ = constructor -// NOTE: this can be set on either constructor-mixin or proto but -// not on both... -// NOTE: if .__proto__ is not set in the proto, then it will be -// set to .__extends__.prototype by default. -// NOTE: setting this and proto.__proto__ to can be used to link the -// constructor and instance object to different prototype chains -// NOTE: this attr is only used if explicitly defined, inherited -// values are ignored. -// XXX this may get removed in future versions. -// -// If true do not link function methods if .__call__(..) is defined -// .__skip_call_attrs__ = bool -// -// -// Special methods (constructor): -// -// Handle uninitialized instance construction -// .__rawinstance__(context, ...) -// -> instance -// NOTE: This is a shorthand to RawInstance(..) see it for -// details. -// -// -// Special methods (.prototype): -// -// Create new instance object... -// .__new__(context, ..) -// -> object -// -// Handle instance call... -// .__call__(context, ..) -// -> .. -// -// Initialize instance object... -// .__init__(..) -// -> .. -// -// -// NOTE: raw instance creation is defined by RawInstance(..) so see -// it for more info. -// NOTE: raw instance creation can be completely overloaded by defining -// .__rawinstance__(..) on the constructor. -// NOTE: if constructor-mixin's .__proto__ is set it will also be copied -// to the created constructor... -// -// -// -// Inheritance: -// A simple way to build C -> B -> A chain would be: -// -// // NOTE: new is optional... -// var A = new Constructor('A') -// -// var B = Constructor('B', A, {}) -// -// var C = Constructor('C', B, {}) -// -// var c = C() -// -// c instanceof C // -> true -// c instanceof B // -> true -// c instanceof A // -> true -// -// A.prototype.x = 123 -// -// c.x // -> 123 -// -// -// -// NOTE: this sets the proto's .constructor attribute, thus rendering it -// not reusable, to use the same prototype for multiple objects -// clone it via. Object.create(..) or copy it... -// NOTE: to disable .__rawinstance__(..) handling set it to false in the -// class prototype... -// NOTE: it is currently not possible to mix native unrelated types, for -// example a callable array constructor will produce inconsistent -// instance objects that in general will not work as expected... -// Reflect.construct(Array, [], Function) -// or -// Reflect.construct(Function, [], Array) -// will either initialize internal/hidden state for either one or -// the other producing a semi-broken instance. -// It is however possible to mix related types as we are doing for -// callable instances (Function + Object -- a function is an object). -// See README.md for more info. -// NOTE: making an object callable does not guarantee that it will pass -// the instanceof Function test, for that the prototype chain needs -// to be rooted in Function. -// though the typeof(..) == 'function' will always work. -// NOTE: this will fail with non-identifier names... -// XXX is this a bug or a feature??? =) -var Constructor = -module.Constructor = -// shorthand... -module.C = -function Constructor(name, a, b, c){ - var args = [...arguments].slice(1, 4) - - // parse args... - // Constructor(name[[, constructor[, mixin]], proto]) - var proto = args.pop() || {} - var constructor_proto = typeof(args[0]) == 'function' ? - args.shift() - : undefined - var constructor_mixin = args.pop() - - // handle: - // Constructor(name, constructor, ..) - // - // NOTE: this is a bit too functional in style by an if-tree would - // be more bulky and less readable... - constructor_proto - // XXX need a better test -- need to test if .__proto__ was set - // manually and not mess it up... - && (proto.__proto__ === Object.prototype - || proto.__proto__ === Function.prototype) - && (proto.__proto__ = constructor_proto.prototype) - // restore func .toString(..) that was replaced to object's .toString(..) - // in the previous op but only if it was not set by user... - && (typeof(proto) == 'function' - && proto.toString === Object.prototype.toString) - // XXX should we wrap this in normalizeIndent(..) ??? - && (proto.toString = Function.prototype.toString) - - // handle: .__extends__ - if(!constructor_proto){ - // handle .__extends__ - a = Object.hasOwnProperty.call(proto, '__extends__') - && proto.__extends__ - b = constructor_mixin != null - && Object.hasOwnProperty.call(constructor_mixin, '__extends__') - && constructor_mixin.__extends__ - // sanity check... - if(!!a && !!b){ - throw new Error('Constructor(..): ' - +'only one of prototype.__extends__ or constructor.__extends__ ' - +'can exist.') } - constructor_proto = !!a ? a : b - // cleanup... - if(!!b){ - constructor_mixin = mixinFlat({}, constructor_mixin) - delete constructor_mixin.__extends__ } - !!constructor_proto - && (proto.__proto__ = constructor_proto.prototype) } - - // the constructor base... - /* c8 ignore next 9 */ - var _constructor = function Constructor(){ - // create raw instance... - var obj = _constructor.__rawinstance__ ? - _constructor.__rawinstance__(this, ...arguments) - : RawInstance(this, _constructor, ...arguments) - // initialize... - obj.__init__ instanceof Function - && obj.__init__(...arguments) - return obj } - - _constructor.name = name - // just in case the browser refuses to change the name, we'll make - // it a different offer ;) - _constructor.name == 'Constructor' - // NOTE: this eval(..) should not be a risk as its inputs are - // static and never infuenced by external inputs... - // NOTE: this will fail with non-identifier names... - && eval('_constructor = '+ _constructor - .toString() - .replace(/Constructor/g, name)) - // set .toString(..)... - // NOTE: do this only if .toString(..) is not defined by user... - // XXX revise this test... - ;((constructor_mixin || {}).toString === Function.prototype.toString - || (constructor_mixin || {}).toString === Object.prototype.toString) - && Object.defineProperty(_constructor, 'toString', { - value: function(){ - var args = proto.__init__ ? - proto.__init__ - .toString() - .split(/\n/)[0] - .replace(/function\(([^)]*)\){.*/, '$1') - : '' - var code = proto.__init__ ? - proto.__init__ - .toString() - .replace(/[^{]*{/, '{') - : '{ .. }' - return `${this.name}(${args})${module.normalizeIndent(code)}` }, - enumerable: false, - }) - // set generic raw instance constructor... - _constructor.__rawinstance__ instanceof Function - || (_constructor.__rawinstance__ = - function(context, ...args){ - return RawInstance(context, this, ...args) }) - !!constructor_proto - && (_constructor.__proto__ = constructor_proto) - _constructor.prototype = proto - _constructor.prototype.constructor = _constructor - - // NOTE: this is intentionally last, this enables the user to override - // any of the system methods... - // NOTE: place the non-overridable definitions after this... - !!constructor_mixin - && mixinFlat( - _constructor, - constructor_mixin) - // also transfer non-default constructor_mixin.__proto__ - && constructor_mixin.__proto__ !== Object.prototype - && (_constructor.__proto__ = constructor_mixin.__proto__) - - // link function stuff for convenience... - proto.__call__ && !(proto instanceof Function) - && _constructor.__skip_call_attrs__ !== true - && module.LINK_FUNCTION_METHODS - .forEach(function(n){ - proto[n] - || Object.defineProperty(proto, n, - Object.assign( - Object.getOwnPropertyDescriptor(Function.prototype, n), - // NOTE: we can't use Function[n] directly because - // they in general test this for relation to - // function which will fail here... - { value: function(){ - return this.__call__[n](this, ...arguments) }, })) }) - - return _constructor } - - - -//--------------------------------------------------------------------- -// For more info see .sources(..) above... -module.STOP = Constructor('STOP', { - doc: 'stop iteration.', - __init__: function(value){ - this.value = value }, -}) - - - - -/********************************************************************** -* vim:set ts=4 sw=4 : */ return module }) +/********************************************************************** +* +* object.js +* +* This is a set of tools and abstractions to create and manage +* constructors, objects and prototype chains in idiomatic JavaScript. +* +* Motivation: +* This package was originally written to unify low level object +* definitios within a large project and from there evolved to be a +* full functional alternative to the ES6 class notation with all of +* its inconsistencies, hoops, "the same but slightly different" ways +* to do things and "magic" (hidden) functionality. +* +* Repo and docs: +* https://github.com/flynx/object.js +* +* +***********************************************/ /* c8 ignore next 2 */ +((typeof define)[0]=='u'?function(f){module.exports=f(require)}:define) +(function(require){ var module={} // make module AMD/node compatible... +/*********************************************************************/ + + +// Function methods to link into a constructor producing a callable +// defined via .__call__(..) +// +// These are needed to support the expected popular function API in a +// callable potentially not related to a function. +// +// see: Constructor(..) for details. +module.LINK_FUNCTION_METHODS = [ + 'call', + 'apply', + 'bind', +] + + + + +//--------------------------------------------------------------------- +// Helpers... + +module.TAB_SIZE = 4 + +module.LEADING_TABS = 1 + + +// Normalize code indent... +// +// normalizeIndent(text) +// -> text +// +// +// This will remove common indent from each line of text, this is useful +// for printing function code of functions that were defined at deep +// levels of indent. +// +// This will ignore the indent of the first line. +// +// If the last line is indented higher or equal to the rest of the text +// we will use leading_tabs (defaults to LEADING_TABS) to indent the +// rest of the text. +// This will indent the following styles correctnly: +// +// |function(a, b){ |function(a, b){ +// | return a + b } | return a + b +// | |} +// +// +// NOTE: this will trim out both leading and trailing white-space. +// NOTE: this is generally code-agnostic with one sigificant +// exception -- normalizeIndent(..) will break code written +// in Whitespace. +// +// XXX is this the right place for this??? +// ...when moving take care that ImageGrid's core.doc uses this... +var normalizeIndent = +module.normalizeIndent = +function(text, tab_size, leading_tabs){ + tab_size = tab_size == null ? + module.TAB_SIZE + : tab_size + leading_tabs = (leading_tabs == null ? + module.LEADING_TABS + : leading_tabs) + * tab_size + // prepare text... + var tab = ' '.repeat(tab_size || 0) + text = tab != '' ? + text.replace(/\t/g, tab) + : text + // trim the tail and remove leading blank lines... + var lines = text.trimEnd().split(/\n/) + while(lines.length > 0 + && lines[0].trim() == ''){ + // XXX we have two options here: + // - indent everyline including the first non-blank + // - do not indent anything (current) + // ...not sure which is best... + leading_tabs = 0 + lines.shift() } + // count common indent... + var l = lines + .reduce(function(l, e, i){ + var indent = e.length - e.trimLeft().length + return e.trim().length == 0 + // ignore 0 indent of first line... + || (i == 0 && indent == 0) ? + l + // last line... + : i == lines.length-1 + && indent >= l ? + // XXX feels a bit overcomplicated... + (l < 0 ? + // last of two with 0 indent on first -> indent... + Math.max(indent - leading_tabs, 0) + // ignore leading_tabs if lower indent... + : Math.min(l, Math.max(indent - leading_tabs, 0))) + // initial state... + : l < 0 ? + indent + // min... + : Math.min(l, indent) }, -1) || 0 + // normalize... + return lines + .map(function(line, i){ + return i == 0 ? + line + : line.slice(l) }) + .join('\n') + .trim() } + + +// shorthand more suted for text... +var normalizeTextIndent = +module.normalizeTextIndent = +function(text, tab_size, leading_tabs){ + return module.normalizeIndent(text, tab_size, leading_tabs || 0) } + + +// template string tag versions of the above... +var doc = +module.doc = +function(strings, ...values){ + return normalizeIndent(strings + .map(function(s, i){ return s + (values[i] || '') }) + .join('')) } + +var text = +module.text = +function(strings, ...values){ + return normalizeTextIndent(strings + .map(function(s, i){ return s + (values[i] || '') }) + .join('')) } + + +// Get keys from prototype chain... +// +// deepKeys(obj) +// deepKeys(obj, stop) +// -> keys +// +// +// NOTE: this is like Object.keys(..) but will get keys for all levels +// till stop if given... +// +// XXX should we add this to Object??? +var deepKeys = +module.deepKeys = +function(obj, stop){ + var res = [] + while(obj != null){ + res.push(Object.keys(obj)) + if(obj === stop){ + break } + obj = obj.__proto__ } + return [...(new Set(res.flat()))] } + + +// Match two objects... +// +// match(a, b) +// -> bool +// +// +// This will match objects iff: +// - if they are identical or +// - attr count is the same and, +// - attr names are the same and, +// - attr values are identical. +// +// +// Non-strict match... +// match(a, b, true) +// +// This is similar to the default case but uses equality rather than +// identity to match values. +// +// +// NOTE: this will do a shallow test using Object.keys(..) thus .__proto__ +// attributes are ignored... +var match = +module.match = +function(base, obj, non_strict){ + // identity... + if(base === obj){ + return true } + // typeof -- sanity check... + if(typeof(base) != typeof(obj)){ + return false } + // attr count... + var o = Object.keys(Object.getOwnPropertyDescriptors(obj)) + if(Object.keys(Object.getOwnPropertyDescriptors(base)).length != o.length){ + return false } + // names and values... + o = o.map(function(k){ + return [k, obj[k]] }) + while(o.length > 0){ + var [k, v] = o.pop() + if(!base.hasOwnProperty(k) + || (non_strict ? + base[k] != v + : base[k] !== v)){ + return false } } + return true } + + +// Like .match(..) but will test if obj is a non-strict subset of base... +// +// NOTE: this will only check direct attributes of both base and obj. +var matchPartial = +module.matchPartial = +function(base, obj, non_strict){ + return base === obj + || Object.entries(obj) + .filter(function([n, v]){ + return !base.hasOwnProperty(n) + || (non_strict ? + base[n] != v + : base[n] !== v) }) + .length == 0 } + + + +//--------------------------------------------------------------------- +// Prototype chain content access... + +// object to trigger iteration stop... +// +// NOTE: this is a placeholder for documnetation/context purposes, see +// the actual implementation at the end of the module... +module.STOP = undefined + + +// Get a list of source objects for a prop/attr name... +// +// sources(obj, name) +// sources(obj, name, callback) +// -> list +// -> [] +// +// Get callables or objects defining .__call__ (special-case) +// sources(obj, '__call__') +// sources(obj, '__call__', callback) +// -> list +// -> [] +// +// Get full chain... +// sources(obj) +// sources(obj, callback) +// -> list +// +// +// callback(obj) +// -> STOP +// -> STOP(value) +// -> .. +// +// +// The callback(..) is called with each matching object. +// +// callback(..) return values: +// - STOP - stop the search and return the match list terminated +// with the object triggering the stop. +// - STOP(value) - stop the search and return the match list terminated +// with the value passed to STOP(..) +// - undefined - return the triggering object as-is +// NOTE: this is the same as returning [obj] +// - array - merge array content into the result insteaad of +// the triggering value. +// NOTE: an ampty array will effectively omit the +// triggering object from the results. +// - other - return a value instead of the triggering object. +// +// +// NOTE: this gos up the prototype chain, not caring about any role ( +// instance/class or instance/prototype) bounderies and depends +// only on the object given as the starting point. +// It is possible to start the search from this, thus checking +// for any overloading in the instance, though this approach is +// not very reusable.... +// NOTE: this will not trigger any props... +var sources = +module.sources = +function(obj, name, callback){ + // get full chain... + if(typeof(name) == 'function'){ + callback = name + name = undefined + } + var o + var res = [] + while(obj != null){ + //if(obj.hasOwnProperty(name)){ + if(name === undefined + || obj.hasOwnProperty(name) + || (name == '__call__' && typeof(obj) == 'function')){ + // handle callback... + o = callback + && callback(obj) + // manage results... + res.push( + (o === undefined || o === module.STOP) ? + [obj] + : o instanceof module.STOP ? + o.value + : o ) + // stop... + if(o === module.STOP + || o instanceof module.STOP){ + return res.flat() } } + obj = obj.__proto__ } + return res.flat() } + + +// Get a list of values/props set in source objects for a prop/attr name... +// +// Get values... +// values(obj, name) +// values(obj, name, callback) +// -> list +// -> [] +// +// Get propery descriptors... +// values(obj, name, true) +// values(obj, name, callback, true) +// -> list +// -> [] +// +// callback(value/prop, obj) +// -> STOP +// -> STOP(value) +// -> .. +// +// +// Special case: name is given as '__call__' +// This will return either the value the object if it is callable +// or the value of .__call__ attribute... +// +// +// NOTE: for more docs on the callback(..) see sources(..) +var values = +module.values = +function(obj, name, callback, props){ + props = callback === true ? + callback + : props + var _get = function(obj, name){ + return props ? + Object.getOwnPropertyDescriptor(obj, name) + // handle callable instance... + : !(name in obj) + && name == '__call__' + && typeof(obj) == 'function' ? + obj + // normal attr... + : obj[name] } + // wrap the callback if given... + var c = typeof(callback) == 'function' + && function(obj){ + var val = _get(obj, name) + var res = callback(val, obj) + return res === module.STOP ? + // wrap the expected stop result if the user did not do it... + module.STOP(val) + : res } + return c ? + // NOTE: we do not need to handle the callback return values as + // this is fully done by c(..) in sources(..) + sources(obj, name, c) + : sources(obj, name) + .map(function(obj){ + return _get(obj, name) }) } + + +// Find the next parent attribute in the prototype chain. +// +// Get parent attribute value... +// parent(proto, name) +// -> value +// -> undefined +// +// Get parent callable or .__call__ value (special-case) +// parent(proto, '__call__') +// -> value +// -> undefined +// +// Get parent method... +// parent(method, this) +// -> meth +// -> undefined +// +// Get parent object... +// parent(this) +// -> parent +// +// +// The two forms differ in: +// - in parent(method, ..) a method's .name attr is used for name. +// - in parent(method, ..) the containing prototype is inferred. +// +// NOTE: there are cases where method.name is not set (e.g. anonymous +// function), so there a name should be passed explicitly... +// NOTE: when passing a method it is recommended to pass an explicit +// reference to it relative to the constructor, i.e.: +// Constructor.prototype.method +// this will avoid relative resolution loops, for example: +// this.method +// deep in a chain will resolve to the first .method value visible +// from 'this', i.e. the top most value and not the value visible +// from that particular level... +// +// +// Example: +// var X = object.Constructor('X', { +// __proto__: Y.prototype, +// +// attr: 123, +// +// method: function(){ +// // get attribute... +// var a = object.parent(X.prototype, 'attr') +// +// // get method... +// var ret = object.parent(X.prototype.method, this) +// .call(this, ...arguments) +// +// // ... +// } +// }) +// +// +// NOTE: in the general case this will get the value of the returned +// property/attribute, the rest of the way passive to props. +// The method case will get the value of every method from 'this' +// and to the method after the match. +// NOTE: this is super(..) replacement, usable in any context without +// restriction -- super(..) is restricted to class methods only... +var parent = +module.parent = +function(proto, name){ + // special case: get parent... + if(arguments.length == 1){ + return proto.__proto__ } + // special case: get method... + if(typeof(name) != typeof('str')){ + var that = name + name = proto.name + // sanity check... + if(name == ''){ + throw new Error('parent(..): need a method with non-empty .name') } + // get first matching source... + proto = sources(that, name, + function(obj){ + return obj[name] === proto + && module.STOP }) + .pop() } + // get first source... + var c = 0 + var res = sources(proto, name, + function(obj){ + return c++ == 1 + && module.STOP }) + .pop() + return !res ? + undefined + :(!(name in res) && typeof(res) == 'function') ? + res + : res[name] } + + +// Find the next parent property descriptor in the prototype chain... +// +// parentProperty(proto, name) +// -> prop-descriptor +// +// +// This is like parent(..) but will get a property descriptor... +var parentProperty = +module.parentProperty = +function(proto, name){ + // get second source... + var c = 0 + var res = sources(proto, name, + function(obj){ + return c++ == 1 + && module.STOP }) + .pop() + return res ? + // get next value... + Object.getOwnPropertyDescriptor(res, name) + : undefined } + + +// Find the next parent method and call it... +// +// parentCall(proto, name, this, ..) +// parentCall(meth, this, ..) +// -> res +// -> undefined +// +// +// This also gracefully handles the case when no higher level definition +// is found, i.e. the corresponding parent(..) call will return undefined +// or a non-callable. +// +// NOTE: this is just like parent(..) but will call the retrieved method, +// essentially this is a shorthand to: +// parent(proto, name).call(this, ...) +// or: +// parent(method, this).call(this, ...) +// NOTE: for more docs see parent(..) +var parentCall = +module.parentCall = +function(proto, name, that, ...args){ + var meth = parent(proto, name) + return typeof(meth) == 'function' ? + meth.call(...( typeof(name) == typeof('str') ? + [...arguments].slice(2) + : [...arguments].slice(1) )) + : undefined } + + +// Test if child is related to parent... +// +// parentOf(parent, child) +// -> bool +// +// +// NOTE: this is like a instanceof b but within the prototype chain +var parentOf = +module.parentOf = +function(parent, child){ + return new Set(sources(child)).has(parent) } + +// Reverse of parentOf(..) +var childOf = +module.childOf = +function(child, parent){ + return parentOf(parent, child) } + +var related = +module.related = +function(a, b){ + return parentOf(a, b) + || parentOf(b, a) } + + + +//--------------------------------------------------------------------- +// Constructor... + +// Make an uninitialized instance object... +// +// RawInstance(context, constructor, ...) +// -> instance +// +// +// This will: +// - construct an object +// - if .__new__(..) is defined +// -> call and use its return value +// - if prototype is a function or if .__call__(..) is defined +// -> use a wrapper function +// - if construct.__proto__ has .__rawinstance__(..) +// -> use it to create an instance +// - if constructor.__proto__ is a constructor +// -> use it to create an instance +// - else +// -> use {} +// - link the object into the prototype chain +// +// +// This will not call .__init__(..), hence the "uninitialized". +// +// +// NOTE: "context" is only used when passeding to .__new__(..) if defined, +// and is ignored otherwise... +// NOTE: as this is simply an extension to the base JavaScript protocol this +// can be used to construct any object... +// Example: +// // new is optional... +// var l = new RawInstance(null, Array, 'a', 'b', 'c') +// NOTE: the following are not the same in structure but functionally +// are identical: +// var C = Constructor('C', function(){ .. }) +// and +// var C2 = Constructor('C2', { __call__: function(){ .. } }) +// the difference is in C.prototype vs. C2.prototype, the first +// being a function while the second is an object with a call +// method... +// NOTE: essentially this is an extended version of Reflect.construct(..) +var RawInstance = +module.RawInstance = +function(context, constructor, ...args){ + var _mirror_doc = function(func, target){ + Object.defineProperty(func, 'toString', { + value: function(...args){ + // user-defined .toString... + if(target.prototype.toString !== Function.prototype.toString){ + return target.prototype.toString.call(this, ...args) } + var f = typeof(target.prototype) == 'function' ? + target.prototype + : target.prototype.__call__ + return typeof(f) == 'function' ? + module.normalizeIndent(f.toString(...args)) + : undefined }, + enumerable: false, + }) + return func } + + var obj = + // prototype defines .__new__(..)... + constructor.prototype.__new__ instanceof Function ? + constructor.prototype.__new__(context, ...args) + // native constructor... + : /\[native code\]/.test(constructor.toString()) ? + Reflect.construct(constructor, args) + // callable instance... + // NOTE: we need to isolate the callable from instances, thus we + // reference 'constructor' directly rather than using + // 'this.constructor'... + : (typeof(constructor.prototype) == 'function' + || constructor.prototype.__call__ instanceof Function) ? + _mirror_doc( + function(){ + return ( + // .prototype is a function... + typeof(constructor.prototype) == 'function' ? + // NOTE: we are not using .call(..) here as it + // may not be accesible through the prototype + // chain, this can occur when creating a + // callable instance from a non-callable + // parent... + Reflect.apply( + constructor.prototype, obj, [this, ...arguments]) + // .__call__(..) or fail semi-gracefully... + : constructor.prototype.__call__ + .call(obj, this, ...arguments)) }, + constructor) + // recursively call .__rawinstance__(..) + : constructor.__proto__.__rawinstance__ ? + constructor.__proto__.__rawinstance__(context, ...args) + // use parent's constructor... + : (typeof(constructor.__proto__) == 'function' + // XXX for some reason if using (function(){}).__proto__ + // instead of Function.prototype below coverage is + // not counted for the condition.... + && constructor.__proto__ !== Function.prototype) ? + Reflect.construct(constructor.__proto__, args, constructor) + // default object base... + : Reflect.construct(Object, [], constructor) + + // link to prototype chain, if not done already... + obj.__proto__ !== constructor.prototype + && (obj.__proto__ = constructor.prototype) + + return obj } + + +// Make an object constructor function... +// +// Make a constructor with an object prototype... +// Constructor(name, proto) +// -> constructor +// +// Make a constructor with a prototype and a constructor prototype... +// Constructor(name, constructor-mixin, proto) +// -> constructor +// +// Make a constructor with prototype extending parent-constructor... +// Constructor(name, parent-constructor, proto) +// Constructor(name, parent-constructor, constructor-mixin, proto) +// -> constructor +// +// +// The resulting constructor can produce objects in one of these ways: +// +// Create instance... +// constructor(..) +// new constructor +// new constructor(..) +// -> instance +// +// Create raw/uninitialized instance... +// constructor.__rawinstance__(..) +// RawInstance(null, constructor, ..) +// -> raw-instance +// +// +// All produced objects are instances of the constructor +// instance instanceof constructor +// -> true +// +// +// +// Create and initialization protocol: +// 1) raw instance is created: +// a) constructor.__rawinstance__(..) / RawInstance(..) called: +// - call .__new__(..) if defined and get return value as +// instance, or +// - if .__call__(..) defined or prototype is a function, wrap +// it and use the wrapper function as instance, or +// - create an empty object +// b) instance linked to prototype chain +// set .__proto__ to constructor.prototype +// 2) instance is initialized: +// call .__init__(..) if defined +// +// +// Special attributes: +// +// Sets parent constructor +// .__extends__ = constructor +// NOTE: this can be set on either constructor-mixin or proto but +// not on both... +// NOTE: if .__proto__ is not set in the proto, then it will be +// set to .__extends__.prototype by default. +// NOTE: setting this and proto.__proto__ to can be used to link the +// constructor and instance object to different prototype chains +// NOTE: this attr is only used if explicitly defined, inherited +// values are ignored. +// XXX this may get removed in future versions. +// +// If true do not link function methods if .__call__(..) is defined +// .__skip_call_attrs__ = bool +// +// +// Special methods (constructor): +// +// Handle uninitialized instance construction +// .__rawinstance__(context, ...) +// -> instance +// NOTE: This is a shorthand to RawInstance(..) see it for +// details. +// +// +// Special methods (.prototype): +// +// Create new instance object... +// .__new__(context, ..) +// -> object +// +// Handle instance call... +// .__call__(context, ..) +// -> .. +// +// Initialize instance object... +// .__init__(..) +// -> .. +// +// +// NOTE: raw instance creation is defined by RawInstance(..) so see +// it for more info. +// NOTE: raw instance creation can be completely overloaded by defining +// .__rawinstance__(..) on the constructor. +// NOTE: if constructor-mixin's .__proto__ is set it will also be copied +// to the created constructor... +// +// +// +// Inheritance: +// A simple way to build C -> B -> A chain would be: +// +// // NOTE: new is optional... +// var A = new Constructor('A') +// +// var B = Constructor('B', A, {}) +// +// var C = Constructor('C', B, {}) +// +// var c = C() +// +// c instanceof C // -> true +// c instanceof B // -> true +// c instanceof A // -> true +// +// A.prototype.x = 123 +// +// c.x // -> 123 +// +// +// +// NOTE: this sets the proto's .constructor attribute, thus rendering it +// not reusable, to use the same prototype for multiple objects +// clone it via. Object.create(..) or copy it... +// NOTE: to disable .__rawinstance__(..) handling set it to false in the +// class prototype... +// NOTE: it is currently not possible to mix native unrelated types, for +// example a callable array constructor will produce inconsistent +// instance objects that in general will not work as expected... +// Reflect.construct(Array, [], Function) +// or +// Reflect.construct(Function, [], Array) +// will either initialize internal/hidden state for either one or +// the other producing a semi-broken instance. +// It is however possible to mix related types as we are doing for +// callable instances (Function + Object -- a function is an object). +// See README.md for more info. +// NOTE: making an object callable does not guarantee that it will pass +// the instanceof Function test, for that the prototype chain needs +// to be rooted in Function. +// though the typeof(..) == 'function' will always work. +// NOTE: this will fail with non-identifier names... +// XXX is this a bug or a feature??? =) +var Constructor = +module.Constructor = +// shorthand... +module.C = +function Constructor(name, a, b, c){ + var args = [...arguments].slice(1, 4) + + // parse args... + // Constructor(name[[, constructor[, mixin]], proto]) + var proto = args.pop() || {} + var constructor_proto = typeof(args[0]) == 'function' ? + args.shift() + : undefined + var constructor_mixin = args.pop() + + // handle: + // Constructor(name, constructor, ..) + // + // NOTE: this is a bit too functional in style by an if-tree would + // be more bulky and less readable... + constructor_proto + // XXX need a better test -- need to test if .__proto__ was set + // manually and not mess it up... + && (proto.__proto__ === Object.prototype + || proto.__proto__ === Function.prototype) + && (proto.__proto__ = constructor_proto.prototype) + // restore func .toString(..) that was replaced to object's .toString(..) + // in the previous op but only if it was not set by user... + && (typeof(proto) == 'function' + && proto.toString === Object.prototype.toString) + // XXX should we wrap this in normalizeIndent(..) ??? + && (proto.toString = Function.prototype.toString) + + // handle: .__extends__ + if(!constructor_proto){ + // handle .__extends__ + a = Object.hasOwnProperty.call(proto, '__extends__') + && proto.__extends__ + b = constructor_mixin != null + && Object.hasOwnProperty.call(constructor_mixin, '__extends__') + && constructor_mixin.__extends__ + // sanity check... + if(!!a && !!b){ + throw new Error('Constructor(..): ' + +'only one of prototype.__extends__ or constructor.__extends__ ' + +'can exist.') } + constructor_proto = !!a ? a : b + // cleanup... + if(!!b){ + constructor_mixin = mixinFlat({}, constructor_mixin) + delete constructor_mixin.__extends__ } + !!constructor_proto + && (proto.__proto__ = constructor_proto.prototype) } + + // the constructor base... + /* c8 ignore next 9 */ + var _constructor = function Constructor(){ + // create raw instance... + var obj = _constructor.__rawinstance__ ? + _constructor.__rawinstance__(this, ...arguments) + : RawInstance(this, _constructor, ...arguments) + // initialize... + obj.__init__ instanceof Function + && obj.__init__(...arguments) + return obj } + + Object.defineProperty(_constructor, 'name', { value: name }) + // just in case the browser/node refuses to change the name, we'll make + // it a different offer ;) + _constructor.name == 'Constructor' + // NOTE: this eval(..) should not be a risk as its inputs are + // static and never infuenced by external inputs... + // NOTE: this will fail with non-identifier names... + && eval('_constructor = '+ _constructor + .toString() + .replace(/Constructor/g, name)) + // set .toString(..)... + // NOTE: do this only if .toString(..) is not defined by user... + // XXX revise this test... + ;((constructor_mixin || {}).toString === Function.prototype.toString + || (constructor_mixin || {}).toString === Object.prototype.toString) + && Object.defineProperty(_constructor, 'toString', { + value: function(){ + var args = proto.__init__ ? + proto.__init__ + .toString() + .split(/\n/)[0] + .replace(/function\(([^)]*)\){.*/, '$1') + : '' + var code = proto.__init__ ? + proto.__init__ + .toString() + .replace(/[^{]*{/, '{') + : '{ .. }' + return `${this.name}(${args})${module.normalizeIndent(code)}` }, + enumerable: false, + }) + // set generic raw instance constructor... + _constructor.__rawinstance__ instanceof Function + || (_constructor.__rawinstance__ = + function(context, ...args){ + return RawInstance(context, this, ...args) }) + !!constructor_proto + && (_constructor.__proto__ = constructor_proto) + _constructor.prototype = proto + _constructor.prototype.constructor = _constructor + + // NOTE: this is intentionally last, this enables the user to override + // any of the system methods... + // NOTE: place the non-overridable definitions after this... + !!constructor_mixin + && mixinFlat( + _constructor, + constructor_mixin) + // also transfer non-default constructor_mixin.__proto__ + && constructor_mixin.__proto__ !== Object.prototype + && (_constructor.__proto__ = constructor_mixin.__proto__) + + // link function stuff for convenience... + proto.__call__ && !(proto instanceof Function) + && _constructor.__skip_call_attrs__ !== true + && module.LINK_FUNCTION_METHODS + .forEach(function(n){ + proto[n] + || Object.defineProperty(proto, n, + Object.assign( + Object.getOwnPropertyDescriptor(Function.prototype, n), + // NOTE: we can't use Function[n] directly because + // they in general test this for relation to + // function which will fail here... + { value: function(){ + return this.__call__[n](this, ...arguments) }, })) }) + + return _constructor } + + + +//--------------------------------------------------------------------- +// For more info see .sources(..) above... +module.STOP = +Constructor('STOP', { + doc: 'stop iteration.', + __init__: function(value){ + this.value = value }, +}) + + + +//--------------------------------------------------------------------- +// Mixin utils... + +// Mix a set of methods/props/attrs into an object... +// +// mixinFlat(base, object, ...) +// -> base +// +// +// NOTE: essentially this is just like Object.assign(..) but copies +// properties directly rather than copying property values... +// NOTE: this will not transfer several the special variables not listed +// by Object.keys(..). +// This includes things like .__proto__ +// NOTE: this can and will overwrite attributes... +var mixinFlat = +module.mixinFlat = +function(base, ...objects){ + return objects + .reduce(function(base, cur){ + Object.keys(cur) + .map(function(k){ + Object.defineProperty(base, k, + Object.getOwnPropertyDescriptor(cur, k)) }) + return base }, base) } + + +// Mix sets of methods/props/attrs into an object as prototypes... +// +// mixin(base, object, ..) +// -> base +// +// +// This will create a new object per set of methods given and +// mixinFlat(..) the method set into this object leaving the +// original objects intact. +// +// base <-- object1_copy <-- .. <-- objectN_copy <- base.__proto__ +// +// +// NOTE: this will only mix in non-empty objects... +// NOTE: mixing into a constructor will break object creation via new... +// Example: +// class A {} +// class B extends A {} +// +// mixin(B, {x: 123}) +// +// var b = new B() // will break... +// +// This does not affect object.Constructor(..) chains... +// NOTE: mixin(Object.prototype, ..) will fail because Object.prototype.__proto__ +// is imutable... +var mixin = +module.mixin = +function(base, ...objects){ + base.__proto__ = objects + .reduce(function(res, cur){ + return Object.keys(cur).length > 0 ? + module.mixinFlat(Object.create(res), cur) + : res }, base.__proto__) + return base } + + +// Get matching mixins... +// +// mixins(base, object[, callback]) +// mixins(base, list[, callback]) +// -> list +// +// +// callback(base, obj, parent) +// -> STOP +// -> undefined +// +// +// NOTE: this will also match base... +// NOTE: if base matches directly callback(..) will get undefined as parent +// NOTE: for more docs on the callback(..) see sources(..) +var mixins = +module.mixins = +function(base, object, callback){ + object = object instanceof Array ? + object + : [object] + var res = [] + var o + var parent + while(base != null){ + // match each object... + for(var obj of object){ + if(match(base, obj)){ + o = callback + && callback(base, obj, parent) + // manage results... + res.push( + (o === undefined || o === module.STOP) ? + [base] + : o instanceof module.STOP ? + o.value + : o ) + if(o === module.STOP + || o instanceof module.STOP){ + return res.flat() } + // match found, no need to test further... + break } } + parent = base + base = base.__proto__ } + return res.flat() } + + +// Check of base has mixin... +// +// hasMixin(base, mixin) +// -> bool +// +// +// NOTE: to test for a flat mixin directly use .matchPartial(base, object) +var hasMixin = +module.hasMixin = +function(base, object){ + return ( + // normal mixin... + mixins(base, object, function(){ return module.STOP }) + .length > 0 + // flat mixin search... + || sources(base, function(p){ + return matchPartial(p, object) ? + module.STOP + : [] }) + .length > 0 )} + + +// Mix-out sets of methods/props/attrs out of an object prototype chain... +// +// Mix-out first occurrence of each matching object... +// mixout(base, object, ..) +// mixout(base, 'first', object, ..) +// -> base +// +// Mix-out all occurrences of each matching object... +// mixout(base, 'all', object, ..) +// -> base +// +// +// NOTE: this is the opposite to mixin(..) +// NOTE: this used mixins(..) / match(..) to find the relevant mixins, +// see those for more info... +var mixout = +module.mixout = +function(base, ...objects){ + var all = objects[0] == 'all' ? + !!objects.shift() + : objects[0] == 'first' ? + !objects.shift() + : false + var remove = [] + mixins(base, objects, function(match, obj, parent){ + parent && remove.push(parent) + // when removing the first occurrence, don't check for obj again... + all || objects.splice(objects.indexOf(obj), 1) }) + // NOTE: we are removing on a separate stage so as not to mess with + // mixins(..) iterating... + remove + // XXX not sure why this is needed, needs thought... + .reverse() + .forEach(function(p){ + p.__proto__ = p.__proto__.__proto__ }) + return base } + + +// Mixin wrapper/object... +// +// Create a new mixin... +// Mixin(name, data, ..) +// -> mixin +// +// Apply the mixin to an object... +// mixin(obj) +// -> obj +// +// +// Example: +// +// var BasicMixin = Mixin('BasicMixin', { +// ... +// }) +// +// var o = { +// ... +// } +// +// BasicMixin(o) +// +// +var Mixin = +module.Mixin = +Constructor('Mixin', { + name: null, + data: null, + + mode: 'proto', + + isMixed: function(target){ + return hasMixin(target, this.data) }, + mixout: function(target){ + return mixout(target, this.data) }, + + // mixin to target... + __call__: function(_, target, mode=this.mode){ + return mode == 'flat' ? + mixinFlat(target, this.data) + : mixin(target, this.data) }, + + __init__: function(name, ...data){ + // NOTE: .defineProperty(..) is used because this is a function + // and function's .name is not too configurable... + // XXX do we need to configure this better??? + Object.defineProperty(this, 'name', { value: name }) + this.data = mixinFlat({}, + ...data.map(function(e){ + return e instanceof Mixin ? + e.data + : e })) }, +}) + + + + +/********************************************************************** +* vim:set ts=4 sw=4 : */ return module }) diff --git a/package.json b/package.json index 8664626..54f7f4e 100755 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ig-object", - "version": "5.3.0", + "version": "5.4.0", "description": "", "main": "object.js", "scripts": { diff --git a/test.js b/test.js index a359ed1..eda5b41 100755 --- a/test.js +++ b/test.js @@ -1,868 +1,868 @@ -#!/usr/bin/env node -/********************************************************************** -* -* TODO: -* - test: -* - ASAP: method calls from methods from different contexts: -* - constructor -* - instance -* - sub-instance -* - callable -* - .call(..) and friends -* - normalizeIndent(..)/normalizeTextIndent(..) -- tab_size values... -* - object.mixout(..) -* - callback STOP in object.mixins(..) -* - props arg in object.values(..) -* - RawInstance(..).toString() -* -* -* -**********************************************************************/ -((typeof define)[0]=='u'?function(f){module.exports=f(require)}:define) -(function(require){ var module={} // make module AMD/node compatible... -/*********************************************************************/ - -var colors = require('colors') - -var test = require('ig-test') - -var object = require('./object') - - -//--------------------------------------------------------------------- -// helpers... - -// compare two arrays by items... -var arrayCmp = function(a, b){ - var ka = Object.keys(a) - var kb = Object.keys(a) - return a === b - || (a.length == b.length - && ka - // keep only non matching stuff... - .filter(function(k){ - return a[k] !== b[k] - && a[k] != a[k] }) - .length == 0) } - -// a constructor is a thing that starts with a capital and has a .prototype -var constructors = function(obj){ - return Object.entries(obj) - .filter(function([k, o]){ - return !k.startsWith('_') - && k[0] == k[0].toUpperCase() - && o.prototype }) } - -// an instance is a thing that starts with a lowercase and has a .constructor -var instances = function(obj){ - return Object.entries(obj) - .filter(function([k, o]){ - return !k.startsWith('_') - && k[0] == k[0].toLowerCase() - && o.constructor }) } - -// data wrapper... -var data = function(d){ return d } - -// text test runner... -var testText = function(assert, func, input, expect){ - var res - return assert((res = object[func](input)) == expect, - func +'(..):' - +'\n---input---\n' - + (input instanceof Array ? - input[0] - : input) - +'\n---Expected---\n' - + expect - +'\n---Got---\n' - + res - +'\n---') } - - - -//--------------------------------------------------------------------- -// General tests... - -var setups = test.Setups({ - // basic constructor and inheritance... - // - // X - // Y <- A <- B <- C <- D <- E - // - // This will test: - // - object creation - // - basic inheritance - // - general usecase - // - .__extends__ - // - method overloading - // - .parent(..) - // - .parentCall(..) - // - .parentProperty(..) - // - constructor methods (XXX not done...) - // - basic: function(assert){ - var X, Y, A, B, C, D, E - return { - X: X = assert(object.Constructor('X'), `.Constructor(..)`), - Y: Y = assert(object.C('Y', { - method: function(){ - var x - assert( - (x = object.parentCall(Y.prototype.method, this, ...arguments)) === undefined, - 'y.method(..): expected:', undefined, 'got:', x) - return 'Y' - }, - }), `.C(..)`), - - A: A = assert(object.C('A', Y, { - get prop(){ - return 'A.prop' }, - method: function(){ - var x - assert( - (x = object.parentCall(A.prototype.method, this, ...arguments)) == 'Y', - 'a.method(..): expected:', 'Y', 'got:', x) - return 'A' - }, - }), `inherit (gen1)`), - B: B = assert(object.C('B', A, { - // XXX constructor methods... - testRelations: function(){ - assert(object.parentOf(A, this), - 'parentOf(A, B): expected to be true') - assert(object.childOf(this, A), - 'childOf(B, A): expected to be true') - - assert(!object.parentOf(X, this), - 'parentOf(X, B): expected to be false') - - assert(object.parentOf(A, E), - 'parentOf(A, E): expected to be true') - assert(object.childOf(E, A), - 'childOf(E, A): expected to be true') - - assert(object.related(A, E) && object.related(E, A), - 'related(A, E) and related(E, A): expected to be true') - - assert(!object.related(X, E) - && !object.related(E, X), - 'related(X, E) and related(E, X): expected to be flase') - }, - }, { - get prop(){ - return 'B.prop' }, - // XXX methods... - }), `inherit (gen2) with constructor mixin`), - C: C = assert(object.C('C', B, { - method: function(){ - var x - assert( - (x = object.parentCall(C.prototype.method, this, ...arguments)) == 'A', - 'c.method(..): expected:', 'A', 'got:', x) - - assert(this.prop == 'B.prop', - 'get property value') - // NOTE: these get "next visible" not "outside current object", - // this is intentional, the "outside" value is simply - // accessible via: - // C.prototype.prop - assert(object.parent(C.prototype, 'prop') == 'A.prop', - 'get parent property value') - assert(object.parentProperty(C.prototype, 'prop').get() == 'A.prop', - 'get parent property') - - assert(object.parentProperty(C.prototype, 'does-not-exist') === undefined, - 'get non-existent property') - - return 'C' - }, - }), `inherit (gen3)`), - D: D = assert(object.C('D', {}, { - __extends__: C, - method: function(){ - var x - assert( - (x = object.parentCall(D.prototype.method, this, ...arguments)) == 'C', - 'c.method(..): expected:', 'C', 'got:', x) - return 'D' - }, - }), '.__extends__ test'), - E: E = assert(object.C('E', { - __extends__: C, - }, { - method: function(){ - var x - assert( - (x = object.parentCall(D.prototype.method, this, ...arguments)) === undefined, - 'c.method(..): expected:', undefined, 'got:', x) - return 'E' - }, - }), '.__extends__ test'), - } }, - - // initialization... - init: function(assert){ - var A, B, C - return { - // init... - A: A = assert(object.C('A', { - msg: '.__init__()', - __init__: function(){ - this.init_has_run = true }, - test_init: function(){ - this.__created_raw ? - assert(!this.init_has_run, this.msg+' did not run') - : assert(this.init_has_run, this.msg+' run') }, - }), 'basic .__init__(..)'), - // new... - B: B = assert(object.C('B', { - __new__: function(){ - var o = {} - o.new_has_run = true - return o - }, - test_new: function(){ - assert(this.new_has_run, '.__new__() run') }, - }), 'basic .__new__(..)'), - // new + init... - C: C = assert(object.C('C', B, { - msg: '.__init__() after .__new__()', - __init__: function(){ - this.init_has_run = true }, - test_init: A.prototype.test_init, - }), `inherit .__new__()`), - - // XXX - } }, - - // callable instances... - call: function(assert){ - // constructors... - var A, B, C, D, F, G - var res = { - A: A = assert(object.C('A', - function(){ - return 'A' - }), 'callable'), - B: B = assert(object.C('B', { - __non_function: true, - __call__: function(){ - assert( - object.parentCall(B.prototype, '__call__', this, ...arguments) === undefined, - 'call non-existent parent method', 'B') - return 'B' - }, - }), 'callable'), - - C: C = assert(object.C('C', A, {}), 'basic inherit'), - D: D = assert(object.C('D', B, {}), 'basic inherit'), - - E: E = assert(object.C('E', A, - function(){ - assert( - object.parentCall(E.prototype, '__call__', this, ...arguments) == 'A', - 'parrent call') - return 'E' - }), 'call parent'), - F: F = assert(object.C('F', B, { - __call__: function(){ - assert( - object.parentCall(F.prototype, '__call__', this, ...arguments) == 'B', - 'parent call') - return 'F' - }, - }), 'call parent\'s .__call__'), - } - // create instances... - var objs = tests.instance(assert, res) - // all instances must be callable... - // NOTE: not all instances are going to be instanceof Function... - Object.entries(objs) - .forEach(function([k, o]){ - assert(typeof(o) == 'function', 'instance is callable', k) }) - - return Object.assign(res, objs) }, - - // inherit from native constructors... - native: function(assert){ - return [ - Object, - Array, - Number, - Map, - Set, - ].reduce(function(res, type){ - var n = type.name - // direct inherit... - var O = res[n] = - assert(object.C(n, type, {}), 'inherit from '+n) - return res - }, {}) }, - - // compatibility: native constructors... - js_constructors: function(assert){ - return { - Object, - Array, - Number, - Map, - Set, - }}, - // compatibility: prototype tree... - js_prototype: function(assert){ - var a, b, c, d - return { - a: a = { - x: 'a', - method: function(){ - return 'a' }, - }, - b: b = { - __proto__: a, - x: 'b', - }, - c: c = { - __proto__: b, - x: 'c', - method: function(){ - var x, y - assert.array( - object.values(c, 'x'), - ['c', 'a', 'b'], - 'reach all values of attr') - assert.array( - object.values(c, 'x', function(v, o){ - return v.toUpperCase() }), - ['C', 'A', 'B'], - 'reach all values of attr') - assert.array( - object.sources(c, 'method'), - // NOTE: not passing an explicit list as we need - // to account for mixins... - object.sources(c) - .filter(function(s){ - return s.hasOwnProperty('method') }), - 'reach all values of method') - assert( - (x = object.parent(c, 'x')) == 'b', - 'reach parent attr: expected:', 'b', 'got:'.bold.yellow, x) - assert( - (x = object.parentCall(c.method, this)) == (y = a.method()), - 'reach parent method: expected:', y, 'got:'.bold.yellow, x) - return 'c' }, - }, - d: d = { - __proto__: c, - method: function(){ - assert(object.parentCall(d.method, this) == 'c', 'reach parent method', 'd') - return 'd' }, - }, - }}, - // compatibility: class/instance... - js_class: function(assert){ - var X, Y, Z - return { - X: X = class { - x = 'x' - method(){ - return 'x' } - }, - Y: Y = class extends X { - x = 'y' - }, - Z: Z = class extends Y { - x = 'z' - method(){ - // XXX this is almost the same as for js_prototype... - assert.array( - object.values(c, 'x'), - ['z', 'y', 'x'], - 'reach all values of attr (class)') - assert.array( - object.values(c, 'x', function(v, o){ - return v.toUpperCase() }), - ['C', 'A', 'B'], - 'reach all values of attr (class)') - assert.array( - object.sources(c, 'method'), - [Z.prototype, X.prototype], - 'reach all values of method (class)') - assert( - object.parent(c, 'x') == super.x, - 'reach super attr (class)') - assert( - object.parentCall(c.method, this) == super.method(), - 'reach super method (class)') - return 'c' } - }, - }}, -}) - - -var modifiers = test.Modifiers({ - // make gen2-3 constructors... - // - // NOTE: there is almost no need to test below gen3... - gen2: function(assert, setup, gen){ - gen = gen || 2 - return constructors(setup) - .reduce(function(res, [n, O]){ - res[n+'g'+gen] = object.C(n+'g'+gen, O, {}) - return res }, {}) }, - gen3: function(assert, setup){ - return this.gen2(assert, this.gen2(assert, setup), '3') }, - - // create instance clones via Object.create(..) - // - clones: function(assert, setup){ - return instances(setup) - .reduce(function(res, [k, o]){ - res[k] = Object.create(o) - return res }, {}) }, - - // generate instances... - // - // NOTE: these are re-used as tests too... - instance: function(assert, setup, mode){ - return constructors(setup) - .reduce(function(res, [k, O]){ - // native JS constructors do not support no_new or raw modes... - if((mode == 'raw' || mode == 'no_new') && !O.__rawinstance__){ - return res } - // create instance with lowercase name of constructor... - // NOTE: constructor is expected to be capitalized... - var o = res[k.toLowerCase()] = - mode == 'no_new' ? - assert(O(), `new:`, k) - : mode == 'raw' ? - assert(O.__rawinstance__(), `.__rawinstance__()`, k) - : assert(new O(), `new:`, k) - - assert(o instanceof O, `instanceof:`, k) - - O.__proto__ instanceof Function - // XXX need to test this for constructor mixins too... - && !(O.__mixin_constructors && !O.__mixin_flat) - && assert(o instanceof o.constructor.__proto__, `instanceof-nested:`, k) - - assert(o.constructor === O, `.constructor:`, k) - - assert(o.__proto__ === O.prototype, `.__proto__:`, k) - - return res }, {}) }, - instance_no_new: function(assert, setup){ - return this.instance(assert, setup, 'no_new') }, - // NOTE: here we mark the raw instances with .__created_raw, this is - // done to be able to distinguish them from fully initialized - // instances... - instance_raw: function(assert, setup){ - var res = this.instance(assert, setup, 'raw') - Object.values(res) - .forEach(function(e){ - Object.assign( - e, - {__created_raw: true}) }) - return res }, - - // mixins... - // NOTE: running this in flat mode will have side-effects -- overwriting - // existing attributes and methods... - // XXX might be a good idea to get the method name from the context... how? - mixin_instance: function(assert, setup, flat, filter, get){ - filter = filter || instances - var attr = '__mixin_' + filter.name - - var mixin = setup[attr] = { - // NOTE: in the real world mixins should have no state, just - // methods... - __mixin: true, - [attr]: true, - __mixin_flat: !!flat, - - method: function(){ - var res = object.parent(mixin.method, this) !== undefined ? - assert( - object.parentCall(mixin.method, this, ...arguments), - 'mixin method parent call') - : false - return res - || 'mixin' }, - - mixinMethod: function(){ - return 'mixin' }, - } - - mixin[attr] = mixin - filter(setup) - .forEach(function([n, o]){ - o = get ? get(o) : o - // mixin once per chain... - if(!o || o[attr]){ - return } - assert(!object.hasMixin(o, mixin), 'pre mixin test', n) - assert(flat ? - object.mixinFlat(o, mixin) - : object.mixin(o, mixin), - flat ? - 'mixin (flat)' - :'mixin', n) - assert(object.hasMixin(o, mixin), 'mixin test', n) - assert(o.mixinMethod() == 'mixin', 'mixin method call') - }) - return setup }, - mixin_instance_flat: function(assert, setup){ - return this.mixin_instance(assert, setup, true) }, - mixin_constructor: function(assert, setup, flat){ - return this.mixin_instance(assert, setup, false, constructors) }, - mixin_constructor_proto: function(assert, setup, flat){ - return this.mixin_instance(assert, setup, false, constructors, - function(o){ - // skip mixing into Object.prototype... - return o !== Object - && o.prototype }) }, - mixin_constructor_flat: function(assert, setup){ - return this.mixin_constructor_proto(assert, setup, true) }, - /*/ XXX - mixout: function(assert, setup){ - return {} - }, - //*/ - - // sanity checks... - // - // NOTE: these should have no side-effects but since we can run - // them why not run them and verify ;) - get methods(){ return tests.methods }, - get constructor_methods(){ return tests.constructor_methods }, - get callables(){ return tests.callables }, -}) - - -var tests = test.Tests({ - // instance creation... - instance: modifiers.instance, - instance_no_new: modifiers.instance_no_new, - instance_raw: modifiers.instance_raw, - - /*/ XXX - attributes: function(assert, setup){ - return {} }, - //*/ - - // methods... - methods: function(assert, setup){ - instances(setup) - .forEach(function([k, o]){ - object.deepKeys(o) - .forEach(function(m){ - typeof(o[m]) == 'function' - // skip special methods... - && !m.startsWith('__') - && o[m]() }) }) - return setup }, - constructor_methods: function(assert, setup){ - constructors(setup) - .forEach(function([k, O]){ - object.deepKeys(O) - .forEach(function(m){ - typeof(O[m]) == 'function' - // skip special methods... - && !m.startsWith('__') - && O[m]() }) }) - return setup }, - - // callables... - callables: function(assert, setup){ - // test special case .values(x, '__call__') - var test = function(obj, name){ - var a, b - return assert(arrayCmp( - a = object.values(obj, '__call__') - .map(function(func){ - return func.call(obj) }) - .flat(), - // get all callables in prototype chain and call them... - b = object.sources(obj) - .filter(function(o){ - return typeof(o) == 'function' - || o.hasOwnProperty('__call__') }) - .map(function(o){ - return o.hasOwnProperty('__call__') ? - o.__call__.call(obj) - // NOTE: not all callables are instances of Function... - : Reflect.apply(Function.prototype, o, [obj]) })), - 'values of .__call__ of '+ name +': got:', a, 'expected:', b) } - - instances(setup) - .filter(function([_, o]){ - // NOTE: not all callables are instances of Function... - return typeof(o) == 'function' - || !!o.__call__ }) - .forEach(function([k, o]){ - o.__non_function ? - assert(!(o instanceof Function), 'non-instanceof Function', k) - : assert(o instanceof Function, 'instanceof Function', k) - - typeof(o) == 'function' - && assert(o(), 'call', k) - - assert(o.call(), '.call(..)', k) - assert(o.apply(), 'apply(..)', k) - assert(o.bind(null)(), '.bind(..)(..)', k) - - test(o, k) - }) - return setup }, -}) - - -var cases = test.Cases({ - 'edge-cases': function(assert){ - - assert.error('double __extends__ fail', function(){ - var X = object.C('X', { - __extends__: Object, - }, { - __extends__: Function, - }) - }) - - // native constructor... - assert.array( - object.RawInstance(null, Array, 'a', 'b', 'c'), - ['a', 'b', 'c'], - 'native constructor') - assert(object.RawInstance(null, Number, '123') == 123, 'native constructor') - - - var x, y - - // object.match(..) - assert(object.match(x = {a: 123, b: '333', c: []}, x) === true, 'match self') - assert(object.match(x, {a: x.a, b: x.b, c: []}) == false, 'strict mismatch') - assert(object.match(x, {a: 123, b: 333, c: x.c}, true) === true, 'non-strict match') - - // object.matchPartial(..) - assert(object.matchPartial(x, {a: x.a}, true) === true, 'non-strict partial match') - assert(object.matchPartial(x, {a: x.a, b: x.b, c: x.c}) === true, 'strict partial match') - - // object.parent(..) - assert(object.parent({}) === {}.__proto__, 'basic proto') - assert.error('.parent(..) of anonymous function', function(){ - object.parent(function(){}, {}) }) - - // object.values(..) - var obj = Object.create({x: 123}) - obj.x = 321 - - assert.array( - object.values(obj, 'x', true) - .map(function(e){ return e.value }), - // XXX assert ignores the order here -- this should fail... - [123, 321], '.values(.., true) ') - - assert( - object.values(obj, 'x', function(){ return object.STOP })[0] == 321, - // XXX assert ignores the order here -- this should fail... - '.values(.., func) ') - assert( - object.values(obj, 'x', function(){ return object.STOP }, true)[0].value == 321, - // XXX assert ignores the order here -- this should fail... - '.values(.., func, true) ') - assert( - object.values(obj, 'x', function(){ return object.STOP(555) }, true)[0] == 555, - // XXX assert ignores the order here -- this should fail... - '.values(.., func, true) with explicit stop value') - }, - deepKeys: function(assert){ - var a = { - a: true - } - var b = { - __proto__: a, - b: true, - } - var c = { - __proto__: b, - c: true, - } - - assert.array(object.deepKeys(c), ['c', 'b', 'a'], 'full chain') - assert.array(object.deepKeys(c, a), ['c', 'b', 'a'], 'full chain') - assert.array(object.deepKeys(c, b), ['c', 'b'], 'partial chain') - assert.array(object.deepKeys(c, c), ['c'], 'partial chain') - - }, - funcMethods: function(assert){ - var X = object.C('X', { - __call__: function(){ - return true }, - }) - - var x = new X() - - assert(x(), 'x()') - assert(x.call(null), 'x.call(null)') - - var xx = Object.create(x) - - assert(typeof(xx.call) == 'function', 'xx.call is a function') - assert(xx.call(null), 'xx.call(null)') - }, -}) - - - -//--------------------------------------------------------------------- -// Text util tests... - -var text = test.TestSet() -test.Case('text', text) - -// XXX still need to test tab_size values (0, ...) -// ...a good candidate for modifiers... -// XXX naming is not clear -- should be close to automatic... -// XXX still need to test tab_size values (0, ...) -text.setups({ - // sanity checks... - '""': data({ - input: '', - all: '', }), - '"abc"': data({ - input: 'abc', - all: 'abc'}), - 'leading whitespace': data({ - input: '\n\t\tabc', - all: 'abc'}), - 'trailing whitespace': data({ - input: 'abc\n\t \n', - all: 'abc'}), - 'leading and trailing whitespace': data({ - input: '\n\t \tabc\n\t \n', - all: 'abc'}), - - // NOTE: there is no way to know what is the indent of 'a' - // relative to the rest of the text... - text_04: data({ - input: `a - c`, - text: 'a\nc', - doc: 'a\n c'}), - text_05: data({ - input: `a\nc`, - text: 'a\nc', - doc: 'a\nc'}), - text_06: data({ - input: `\ - a - c`, - all: 'a\n c'}), - text_07: data({ - input: ` - a - c`, - all: 'a\n c'}), - text_08: data({ - input: ` - a - c`, - all: 'a\n c' }), - - text_09: data({ - input: `a - b - c`, - text: 'a\nb\nc', - doc: 'a\n b\n c' }), - - text_10: data({ - input: ` - a - b - c`, - all: 'a\nb\n c' }), - text_11: data({ - input: ` - a - b - c`, - all: 'a\n b\n c' }), - - text_12: data({ - input: `a - b - c`, - all: `a\n b\nc` }), - text_13: data({ - input: `a - b - c`, - all: `a\nb\n c` }), - text_14: data({ - input: ` - a - b - c`, - all: `a\n b\n c` }), - text_15: data({ - input: `a - b - b - b - c`, - all: `a\n b\n b\n b\nc` }), - text_16: data({ - input: `a - b - b - b - c`, - all: `a\n b\n b\n b\nc` }), - text_17: data({ - input: ` - a - b - c`, - all: `a\n b\nc` }), - text_18: data({ - input: `a - b - c - d`, - text: `a\n b\nc\nd`, - // XXX not sure about this... - doc: `a\n b\n c\n d` }), -}) - - -text.tests({ - doc: function(assert, data){ - var e = data.doc != null ? - data.doc - : data.all - data.doc != null - && testText(assert, 'normalizeIndent', data.input, data.doc) - && testText(assert, 'doc', [data.input], data.doc) }, - text: function(assert, data){ - var e = data.text != null ? - data.text - : data.all - e != null - && testText(assert, 'normalizeTextIndent', data.input, e) - && testText(assert, 'text', [data.input], e) }, -}) - - - -//--------------------------------------------------------------------- - -typeof(__filename) != 'undefined' - && __filename == (require.main || {}).filename - && test.run() - - - - -/********************************************************************** -* vim:set ts=4 sw=4 : */ return module }) +#!/usr/bin/env node +/********************************************************************** +* +* TODO: +* - test: +* - ASAP: method calls from methods from different contexts: +* - constructor +* - instance +* - sub-instance +* - callable +* - .call(..) and friends +* - normalizeIndent(..)/normalizeTextIndent(..) -- tab_size values... +* - object.mixout(..) +* - callback STOP in object.mixins(..) +* - props arg in object.values(..) +* - RawInstance(..).toString() +* +* +* +**********************************************************************/ +((typeof define)[0]=='u'?function(f){module.exports=f(require)}:define) +(function(require){ var module={} // make module AMD/node compatible... +/*********************************************************************/ + +var colors = require('colors') + +var test = require('ig-test') + +var object = require('./object') + + +//--------------------------------------------------------------------- +// helpers... + +// compare two arrays by items... +var arrayCmp = function(a, b){ + var ka = Object.keys(a) + var kb = Object.keys(a) + return a === b + || (a.length == b.length + && ka + // keep only non matching stuff... + .filter(function(k){ + return a[k] !== b[k] + && a[k] != a[k] }) + .length == 0) } + +// a constructor is a thing that starts with a capital and has a .prototype +var constructors = function(obj){ + return Object.entries(obj) + .filter(function([k, o]){ + return !k.startsWith('_') + && k[0] == k[0].toUpperCase() + && o.prototype }) } + +// an instance is a thing that starts with a lowercase and has a .constructor +var instances = function(obj){ + return Object.entries(obj) + .filter(function([k, o]){ + return !k.startsWith('_') + && k[0] == k[0].toLowerCase() + && o.constructor }) } + +// data wrapper... +var data = function(d){ return d } + +// text test runner... +var testText = function(assert, func, input, expect){ + var res + return assert((res = object[func](input)) == expect, + func +'(..):' + +'\n---input---\n' + + (input instanceof Array ? + input[0] + : input) + +'\n---Expected---\n' + + expect + +'\n---Got---\n' + + res + +'\n---') } + + + +//--------------------------------------------------------------------- +// General tests... + +var setups = test.Setups({ + // basic constructor and inheritance... + // + // X + // Y <- A <- B <- C <- D <- E + // + // This will test: + // - object creation + // - basic inheritance + // - general usecase + // - .__extends__ + // - method overloading + // - .parent(..) + // - .parentCall(..) + // - .parentProperty(..) + // - constructor methods (XXX not done...) + // + basic: function(assert){ + var X, Y, A, B, C, D, E + return { + X: X = assert(object.Constructor('X'), `.Constructor(..)`), + Y: Y = assert(object.C('Y', { + method: function(){ + var x + assert( + (x = object.parentCall(Y.prototype.method, this, ...arguments)) === undefined, + 'y.method(..): expected:', undefined, 'got:', x) + return 'Y' + }, + }), `.C(..)`), + + A: A = assert(object.C('A', Y, { + get prop(){ + return 'A.prop' }, + method: function(){ + var x + assert( + (x = object.parentCall(A.prototype.method, this, ...arguments)) == 'Y', + 'a.method(..): expected:', 'Y', 'got:', x) + return 'A' + }, + }), `inherit (gen1)`), + B: B = assert(object.C('B', A, { + // XXX constructor methods... + testRelations: function(){ + assert(object.parentOf(A, this), + 'parentOf(A, B): expected to be true') + assert(object.childOf(this, A), + 'childOf(B, A): expected to be true') + + assert(!object.parentOf(X, this), + 'parentOf(X, B): expected to be false') + + assert(object.parentOf(A, E), + 'parentOf(A, E): expected to be true') + assert(object.childOf(E, A), + 'childOf(E, A): expected to be true') + + assert(object.related(A, E) && object.related(E, A), + 'related(A, E) and related(E, A): expected to be true') + + assert(!object.related(X, E) + && !object.related(E, X), + 'related(X, E) and related(E, X): expected to be flase') + }, + }, { + get prop(){ + return 'B.prop' }, + // XXX methods... + }), `inherit (gen2) with constructor mixin`), + C: C = assert(object.C('C', B, { + method: function(){ + var x + assert( + (x = object.parentCall(C.prototype.method, this, ...arguments)) == 'A', + 'c.method(..): expected:', 'A', 'got:', x) + + assert(this.prop == 'B.prop', + 'get property value') + // NOTE: these get "next visible" not "outside current object", + // this is intentional, the "outside" value is simply + // accessible via: + // C.prototype.prop + assert(object.parent(C.prototype, 'prop') == 'A.prop', + 'get parent property value') + assert(object.parentProperty(C.prototype, 'prop').get() == 'A.prop', + 'get parent property') + + assert(object.parentProperty(C.prototype, 'does-not-exist') === undefined, + 'get non-existent property') + + return 'C' + }, + }), `inherit (gen3)`), + D: D = assert(object.C('D', {}, { + __extends__: C, + method: function(){ + var x + assert( + (x = object.parentCall(D.prototype.method, this, ...arguments)) == 'C', + 'c.method(..): expected:', 'C', 'got:', x) + return 'D' + }, + }), '.__extends__ test'), + E: E = assert(object.C('E', { + __extends__: C, + }, { + method: function(){ + var x + assert( + (x = object.parentCall(D.prototype.method, this, ...arguments)) === undefined, + 'c.method(..): expected:', undefined, 'got:', x) + return 'E' + }, + }), '.__extends__ test'), + } }, + + // initialization... + init: function(assert){ + var A, B, C + return { + // init... + A: A = assert(object.C('A', { + msg: '.__init__()', + __init__: function(){ + this.init_has_run = true }, + test_init: function(){ + this.__created_raw ? + assert(!this.init_has_run, this.msg+' did not run') + : assert(this.init_has_run, this.msg+' run') }, + }), 'basic .__init__(..)'), + // new... + B: B = assert(object.C('B', { + __new__: function(){ + var o = {} + o.new_has_run = true + return o + }, + test_new: function(){ + assert(this.new_has_run, '.__new__() run') }, + }), 'basic .__new__(..)'), + // new + init... + C: C = assert(object.C('C', B, { + msg: '.__init__() after .__new__()', + __init__: function(){ + this.init_has_run = true }, + test_init: A.prototype.test_init, + }), `inherit .__new__()`), + + // XXX + } }, + + // callable instances... + call: function(assert){ + // constructors... + var A, B, C, D, F, G + var res = { + A: A = assert(object.C('A', + function(){ + return 'A' + }), 'callable'), + B: B = assert(object.C('B', { + __non_function: true, + __call__: function(){ + assert( + object.parentCall(B.prototype, '__call__', this, ...arguments) === undefined, + 'call non-existent parent method', 'B') + return 'B' + }, + }), 'callable'), + + C: C = assert(object.C('C', A, {}), 'basic inherit'), + D: D = assert(object.C('D', B, {}), 'basic inherit'), + + E: E = assert(object.C('E', A, + function(){ + assert( + object.parentCall(E.prototype, '__call__', this, ...arguments) == 'A', + 'parrent call') + return 'E' + }), 'call parent'), + F: F = assert(object.C('F', B, { + __call__: function(){ + assert( + object.parentCall(F.prototype, '__call__', this, ...arguments) == 'B', + 'parent call') + return 'F' + }, + }), 'call parent\'s .__call__'), + } + // create instances... + var objs = tests.instance(assert, res) + // all instances must be callable... + // NOTE: not all instances are going to be instanceof Function... + Object.entries(objs) + .forEach(function([k, o]){ + assert(typeof(o) == 'function', 'instance is callable', k) }) + + return Object.assign(res, objs) }, + + // inherit from native constructors... + native: function(assert){ + return [ + Object, + Array, + Number, + Map, + Set, + ].reduce(function(res, type){ + var n = type.name + // direct inherit... + var O = res[n] = + assert(object.C(n, type, {}), 'inherit from '+n) + return res + }, {}) }, + + // compatibility: native constructors... + js_constructors: function(assert){ + return { + Object, + Array, + Number, + Map, + Set, + }}, + // compatibility: prototype tree... + js_prototype: function(assert){ + var a, b, c, d + return { + a: a = { + x: 'a', + method: function(){ + return 'a' }, + }, + b: b = { + __proto__: a, + x: 'b', + }, + c: c = { + __proto__: b, + x: 'c', + method: function(){ + var x, y + assert.array( + object.values(c, 'x'), + ['c', 'a', 'b'], + 'reach all values of attr') + assert.array( + object.values(c, 'x', function(v, o){ + return v.toUpperCase() }), + ['C', 'A', 'B'], + 'reach all values of attr') + assert.array( + object.sources(c, 'method'), + // NOTE: not passing an explicit list as we need + // to account for mixins... + object.sources(c) + .filter(function(s){ + return s.hasOwnProperty('method') }), + 'reach all values of method') + assert( + (x = object.parent(c, 'x')) == 'b', + 'reach parent attr: expected:', 'b', 'got:'.bold.yellow, x) + assert( + (x = object.parentCall(c.method, this)) == (y = a.method()), + 'reach parent method: expected:', y, 'got:'.bold.yellow, x) + return 'c' }, + }, + d: d = { + __proto__: c, + method: function(){ + assert(object.parentCall(d.method, this) == 'c', 'reach parent method', 'd') + return 'd' }, + }, + }}, + // compatibility: class/instance... + js_class: function(assert){ + var X, Y, Z + return { + X: X = class { + x = 'x' + method(){ + return 'x' } + }, + Y: Y = class extends X { + x = 'y' + }, + Z: Z = class extends Y { + x = 'z' + method(){ + // XXX this is almost the same as for js_prototype... + assert.array( + object.values(c, 'x'), + ['z', 'y', 'x'], + 'reach all values of attr (class)') + assert.array( + object.values(c, 'x', function(v, o){ + return v.toUpperCase() }), + ['C', 'A', 'B'], + 'reach all values of attr (class)') + assert.array( + object.sources(c, 'method'), + [Z.prototype, X.prototype], + 'reach all values of method (class)') + assert( + object.parent(c, 'x') == super.x, + 'reach super attr (class)') + assert( + object.parentCall(c.method, this) == super.method(), + 'reach super method (class)') + return 'c' } + }, + }}, +}) + + +var modifiers = test.Modifiers({ + // make gen2-3 constructors... + // + // NOTE: there is almost no need to test below gen3... + gen2: function(assert, setup, gen){ + gen = gen || 2 + return constructors(setup) + .reduce(function(res, [n, O]){ + res[n+'g'+gen] = object.C(n+'g'+gen, O, {}) + return res }, {}) }, + gen3: function(assert, setup){ + return this.gen2(assert, this.gen2(assert, setup), '3') }, + + // create instance clones via Object.create(..) + // + clones: function(assert, setup){ + return instances(setup) + .reduce(function(res, [k, o]){ + res[k] = Object.create(o) + return res }, {}) }, + + // generate instances... + // + // NOTE: these are re-used as tests too... + instance: function(assert, setup, mode){ + return constructors(setup) + .reduce(function(res, [k, O]){ + // native JS constructors do not support no_new or raw modes... + if((mode == 'raw' || mode == 'no_new') && !O.__rawinstance__){ + return res } + // create instance with lowercase name of constructor... + // NOTE: constructor is expected to be capitalized... + var o = res[k.toLowerCase()] = + mode == 'no_new' ? + assert(O(), `new:`, k) + : mode == 'raw' ? + assert(O.__rawinstance__(), `.__rawinstance__()`, k) + : assert(new O(), `new:`, k) + + assert(o instanceof O, `instanceof:`, k) + + O.__proto__ instanceof Function + // XXX need to test this for constructor mixins too... + && !(O.__mixin_constructors && !O.__mixin_flat) + && assert(o instanceof o.constructor.__proto__, `instanceof-nested:`, k) + + assert(o.constructor === O, `.constructor:`, k) + + assert(o.__proto__ === O.prototype, `.__proto__:`, k) + + return res }, {}) }, + instance_no_new: function(assert, setup){ + return this.instance(assert, setup, 'no_new') }, + // NOTE: here we mark the raw instances with .__created_raw, this is + // done to be able to distinguish them from fully initialized + // instances... + instance_raw: function(assert, setup){ + var res = this.instance(assert, setup, 'raw') + Object.values(res) + .forEach(function(e){ + Object.assign( + e, + {__created_raw: true}) }) + return res }, + + // mixins... + // NOTE: running this in flat mode will have side-effects -- overwriting + // existing attributes and methods... + // XXX might be a good idea to get the method name from the context... how? + mixin_instance: function(assert, setup, flat, filter, get){ + filter = filter || instances + var attr = '__mixin_' + filter.name + + var mixin = setup[attr] = { + // NOTE: in the real world mixins should have no state, just + // methods... + __mixin: true, + [attr]: true, + __mixin_flat: !!flat, + + method: function(){ + var res = object.parent(mixin.method, this) !== undefined ? + assert( + object.parentCall(mixin.method, this, ...arguments), + 'mixin method parent call') + : false + return res + || 'mixin' }, + + mixinMethod: function(){ + return 'mixin' }, + } + + mixin[attr] = mixin + filter(setup) + .forEach(function([n, o]){ + o = get ? get(o) : o + // mixin once per chain... + if(!o || o[attr]){ + return } + assert(!object.hasMixin(o, mixin), 'pre mixin test', n) + assert(flat ? + object.mixinFlat(o, mixin) + : object.mixin(o, mixin), + flat ? + 'mixin (flat)' + :'mixin', n) + assert(object.hasMixin(o, mixin), 'mixin test', n) + assert(o.mixinMethod() == 'mixin', 'mixin method call') + }) + return setup }, + mixin_instance_flat: function(assert, setup){ + return this.mixin_instance(assert, setup, true) }, + mixin_constructor: function(assert, setup, flat){ + return this.mixin_instance(assert, setup, false, constructors) }, + mixin_constructor_proto: function(assert, setup, flat){ + return this.mixin_instance(assert, setup, false, constructors, + function(o){ + // skip mixing into Object.prototype... + return o !== Object + && o.prototype }) }, + mixin_constructor_flat: function(assert, setup){ + return this.mixin_constructor_proto(assert, setup, true) }, + /*/ XXX + mixout: function(assert, setup){ + return {} + }, + //*/ + + // sanity checks... + // + // NOTE: these should have no side-effects but since we can run + // them why not run them and verify ;) + get methods(){ return tests.methods }, + get constructor_methods(){ return tests.constructor_methods }, + get callables(){ return tests.callables }, +}) + + +var tests = test.Tests({ + // instance creation... + instance: modifiers.instance, + instance_no_new: modifiers.instance_no_new, + instance_raw: modifiers.instance_raw, + + /*/ XXX + attributes: function(assert, setup){ + return {} }, + //*/ + + // methods... + methods: function(assert, setup){ + instances(setup) + .forEach(function([k, o]){ + object.deepKeys(o) + .forEach(function(m){ + typeof(o[m]) == 'function' + // skip special methods... + && !m.startsWith('__') + && o[m]() }) }) + return setup }, + constructor_methods: function(assert, setup){ + constructors(setup) + .forEach(function([k, O]){ + object.deepKeys(O) + .forEach(function(m){ + typeof(O[m]) == 'function' + // skip special methods... + && !m.startsWith('__') + && O[m]() }) }) + return setup }, + + // callables... + callables: function(assert, setup){ + // test special case .values(x, '__call__') + var test = function(obj, name){ + var a, b + return assert(arrayCmp( + a = object.values(obj, '__call__') + .map(function(func){ + return func.call(obj) }) + .flat(), + // get all callables in prototype chain and call them... + b = object.sources(obj) + .filter(function(o){ + return typeof(o) == 'function' + || o.hasOwnProperty('__call__') }) + .map(function(o){ + return o.hasOwnProperty('__call__') ? + o.__call__.call(obj) + // NOTE: not all callables are instances of Function... + : Reflect.apply(Function.prototype, o, [obj]) })), + 'values of .__call__ of '+ name +': got:', a, 'expected:', b) } + + instances(setup) + .filter(function([_, o]){ + // NOTE: not all callables are instances of Function... + return typeof(o) == 'function' + || !!o.__call__ }) + .forEach(function([k, o]){ + o.__non_function ? + assert(!(o instanceof Function), 'non-instanceof Function', k) + : assert(o instanceof Function, 'instanceof Function', k) + + typeof(o) == 'function' + && assert(o(), 'call', k) + + assert(o.call(), '.call(..)', k) + assert(o.apply(), 'apply(..)', k) + assert(o.bind(null)(), '.bind(..)(..)', k) + + test(o, k) + }) + return setup }, +}) + + +var cases = test.Cases({ + 'edge-cases': function(assert){ + + assert.error('double __extends__ fail', function(){ + var X = object.C('X', { + __extends__: Object, + }, { + __extends__: Function, + }) + }) + + // native constructor... + assert.array( + object.RawInstance(null, Array, 'a', 'b', 'c'), + ['a', 'b', 'c'], + 'native constructor') + assert(object.RawInstance(null, Number, '123') == 123, 'native constructor') + + + var x, y + + // object.match(..) + assert(object.match(x = {a: 123, b: '333', c: []}, x) === true, 'match self') + assert(object.match(x, {a: x.a, b: x.b, c: []}) == false, 'strict mismatch') + assert(object.match(x, {a: 123, b: 333, c: x.c}, true) === true, 'non-strict match') + + // object.matchPartial(..) + assert(object.matchPartial(x, {a: x.a}, true) === true, 'non-strict partial match') + assert(object.matchPartial(x, {a: x.a, b: x.b, c: x.c}) === true, 'strict partial match') + + // object.parent(..) + assert(object.parent({}) === {}.__proto__, 'basic proto') + assert.error('.parent(..) of anonymous function', function(){ + object.parent(function(){}, {}) }) + + // object.values(..) + var obj = Object.create({x: 123}) + obj.x = 321 + + assert.array( + object.values(obj, 'x', true) + .map(function(e){ return e.value }), + // XXX assert ignores the order here -- this should fail... + [123, 321], '.values(.., true) ') + + assert( + object.values(obj, 'x', function(){ return object.STOP })[0] == 321, + // XXX assert ignores the order here -- this should fail... + '.values(.., func) ') + assert( + object.values(obj, 'x', function(){ return object.STOP }, true)[0].value == 321, + // XXX assert ignores the order here -- this should fail... + '.values(.., func, true) ') + assert( + object.values(obj, 'x', function(){ return object.STOP(555) }, true)[0] == 555, + // XXX assert ignores the order here -- this should fail... + '.values(.., func, true) with explicit stop value') + }, + deepKeys: function(assert){ + var a = { + a: true + } + var b = { + __proto__: a, + b: true, + } + var c = { + __proto__: b, + c: true, + } + + assert.array(object.deepKeys(c), ['c', 'b', 'a'], 'full chain') + assert.array(object.deepKeys(c, a), ['c', 'b', 'a'], 'full chain') + assert.array(object.deepKeys(c, b), ['c', 'b'], 'partial chain') + assert.array(object.deepKeys(c, c), ['c'], 'partial chain') + + }, + funcMethods: function(assert){ + var X = object.C('X', { + __call__: function(){ + return true }, + }) + + var x = new X() + + assert(x(), 'x()') + assert(x.call(null), 'x.call(null)') + + var xx = Object.create(x) + + assert(typeof(xx.call) == 'function', 'xx.call is a function') + assert(xx.call(null), 'xx.call(null)') + }, +}) + + + +//--------------------------------------------------------------------- +// Text util tests... + +var text = test.TestSet() +test.Case('text', text) + +// XXX still need to test tab_size values (0, ...) +// ...a good candidate for modifiers... +// XXX naming is not clear -- should be close to automatic... +// XXX still need to test tab_size values (0, ...) +text.setups({ + // sanity checks... + '""': data({ + input: '', + all: '', }), + '"abc"': data({ + input: 'abc', + all: 'abc'}), + 'leading whitespace': data({ + input: '\n\t\tabc', + all: 'abc'}), + 'trailing whitespace': data({ + input: 'abc\n\t \n', + all: 'abc'}), + 'leading and trailing whitespace': data({ + input: '\n\t \tabc\n\t \n', + all: 'abc'}), + + // NOTE: there is no way to know what is the indent of 'a' + // relative to the rest of the text... + text_04: data({ + input: `a + c`, + text: 'a\nc', + doc: 'a\n c'}), + text_05: data({ + input: `a\nc`, + text: 'a\nc', + doc: 'a\nc'}), + text_06: data({ + input: `\ + a + c`, + all: 'a\n c'}), + text_07: data({ + input: ` + a + c`, + all: 'a\n c'}), + text_08: data({ + input: ` + a + c`, + all: 'a\n c' }), + + text_09: data({ + input: `a + b + c`, + text: 'a\nb\nc', + doc: 'a\n b\n c' }), + + text_10: data({ + input: ` + a + b + c`, + all: 'a\nb\n c' }), + text_11: data({ + input: ` + a + b + c`, + all: 'a\n b\n c' }), + + text_12: data({ + input: `a + b + c`, + all: `a\n b\nc` }), + text_13: data({ + input: `a + b + c`, + all: `a\nb\n c` }), + text_14: data({ + input: ` + a + b + c`, + all: `a\n b\n c` }), + text_15: data({ + input: `a + b + b + b + c`, + all: `a\n b\n b\n b\nc` }), + text_16: data({ + input: `a + b + b + b + c`, + all: `a\n b\n b\n b\nc` }), + text_17: data({ + input: ` + a + b + c`, + all: `a\n b\nc` }), + text_18: data({ + input: `a + b + c + d`, + text: `a\n b\nc\nd`, + // XXX not sure about this... + doc: `a\n b\n c\n d` }), +}) + + +text.tests({ + doc: function(assert, data){ + var e = data.doc != null ? + data.doc + : data.all + data.doc != null + && testText(assert, 'normalizeIndent', data.input, data.doc) + && testText(assert, 'doc', [data.input], data.doc) }, + text: function(assert, data){ + var e = data.text != null ? + data.text + : data.all + e != null + && testText(assert, 'normalizeTextIndent', data.input, e) + && testText(assert, 'text', [data.input], e) }, +}) + + + +//--------------------------------------------------------------------- + +typeof(__filename) != 'undefined' + && __filename == (require.main || {}).filename + && test.run() + + + + +/********************************************************************** +* vim:set ts=4 sw=4 : */ return module })