From a5bab42336152ed328a5057fa839e3ce3819864a Mon Sep 17 00:00:00 2001 From: "Alex A. Naanou" Date: Sun, 24 May 2020 23:59:01 +0300 Subject: [PATCH] reworked access to parent callable implementations... Signed-off-by: Alex A. Naanou --- README.md | 66 +++++++++++++++++++++-- object.js | 97 +++++++++++++++++++++++++--------- package.json | 2 +- test.js | 144 +++++++++++++++++++++++++++++++++++++++++---------- 4 files changed, 252 insertions(+), 57 deletions(-) diff --git a/README.md b/README.md index fecb913..c0073be 100755 --- a/README.md +++ b/README.md @@ -213,9 +213,16 @@ var Base = object.Constructor('Base', { }) var Item = object.Constructor('Item', Base, { + method: function(){ + // ... + + // call the "super" method... + return object.parentCall(Item.prototype, 'method', this, ...arguments) + }, + __init__: function(){ // call the "super" method... - object.parentCall(this.prototype.__init__, this) + object.parentCall(this.__init__, this, ...arguments) this.item_attr = 'instance attribute value' }, @@ -245,7 +252,8 @@ var Action = object.Constructor('Action', var Action2 = object.Constructor('Action2', { __call__: function(context, ...args){ - return this + // call the callable parent... + return object.parentCall(Action2.prototype, '__call__', this, ...arguments) }, }) @@ -270,6 +278,17 @@ represent the two contexts relevant to the callable instance: If the prototype is explicitly defined as a function then it is the user's responsibility to call `.__call__(..)` method. +When calling the parent passing `'__call__'` will get the parent in both +the function and `.__call__(..)` implementations, but extra care must be +taken in passing the reference prototype to `.parentCall(..)`, the instance +is implemented as a proxy function that will pass the arguments to the +implementation (i.e. `this.constructor.prototype(..)`) so this proxy +function as well as the `.constructor.prototype(..)` are valid implementations +and both will be retrieved by `sources(this, '__call__')`, +`values(this, '__call__')` and by extension `parent(this, '__call__')` +and friends, so this is another reason not to use `this` in the general +case. + **Notes:** - the two approaches (_function_ vs. `.__call__(..)`) will produce @@ -497,6 +516,18 @@ one of the following: `callback(..)` and continue. +Special case: get callable implementations +``` +sources(, '__call__') +sources(, '__call__', ) + -> +``` + +This will get the callable implementations regardless of the actual +implementation details, i.e. both function prototype or `.__call__(..)` +methods will be matched. + + ### `values(..)` Get values for attribute in prototype chain @@ -528,7 +559,7 @@ callback(, ) -> ``` -See [`sources(..)`](#sources) for docs on `callback(..)` +See [`sources(..)`](#sources) for docs on `callback(..)` and special cases. ### `parent(..)` @@ -537,18 +568,37 @@ Get parent attribute value or method parent(, ) -> -> undefined +``` +It is recommended to use the relative`.prototype` as +`` and in turn not recommended to use `this` or `this.__proto__` +as they will not provide the appropriate reference point in the prototype +chain for the current method and may result in infinite recursion. + +For access to parent methods the following special case is better. + +``` parent(, ) -> -> undefined ``` + _Edge case: The `parent(, ..)` has one potential pitfall -- in the rare case where a prototype chain contains two or more references to the same method under the same name, `parent(..)` can't distinguish between these references and will always return the second one._ +Special case: get the parent callable implementation +``` +parent(, '__call__') + -> + -> undefined +``` + +See [`sources(..)`](#sources) for more info on the special case. + ### `parentProperty(..)` @@ -573,6 +623,15 @@ parentCall(, ) -> undefined ``` +Special case: call the parent callable implementation +``` +parentCall(, '__call__', ) + -> + -> undefined +``` + +See [`parent(..)`](#parent) and [`sources(..)`](#sources) for more details. + ### `mixin(..)` @@ -814,4 +873,5 @@ Still, this is worth some thought. Copyright (c) 2019, Alex A. Naanou, All rights reserved. + diff --git a/object.js b/object.js index 63cec6a..d6d347a 100755 --- a/object.js +++ b/object.js @@ -181,13 +181,18 @@ module.STOP = // for any overloading in the instance, though this approach is // not very reusable.... // NOTE: this will not trigger any props... +// +// XXX document the '__call__' cpecial case... +// XXX in the call case need to skip the wrapper function... (???) var sources = module.sources = function(obj, name, callback){ var o var res = [] while(obj != null){ - if(obj.hasOwnProperty(name)){ + //if(obj.hasOwnProperty(name)){ + if(obj.hasOwnProperty(name) + || (name == '__call__' && typeof(obj) == 'function')){ // handle callback... o = callback && callback(obj) @@ -223,27 +228,33 @@ function(obj, name, callback){ // // // NOTE: for more docs on the callback(..) see sources(..) +// +// XXX document the '__call__' cpecial case... 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){ - return callback( - props ? - Object.getOwnPropertyDescriptor(obj, name) - : [ obj[name] ], - obj) } + return callback(_get(obj, name), obj) } return sources(...(c ? [obj, name, c] : [obj, name])) .map(function(obj){ - return props ? - Object.getOwnPropertyDescriptor(obj, name) - : obj[name] }) } + return _get(obj, name) }) } // Find the next parent attribute in the prototype chain. @@ -258,6 +269,10 @@ function(obj, name, callback, props){ // -> 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. @@ -300,9 +315,20 @@ function(obj, name, callback, props){ // 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... +// +// XXX need to be able to get a callable prototype... +// parent(proto, '__call__') +// This would need to handle two cases transparently: +// - parent with .__call__(..) defined... +// - parent with callable prototype... +// ...should this be handled here or in sources(..)??? +// XXX document both __call__ cases... 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 @@ -317,13 +343,17 @@ function(proto, name){ && module.STOP }) .pop() } // get first source... + var c = 0 var res = sources(proto, name, - function(obj){ return module.STOP }) + function(obj){ + return c++ == 1 + && module.STOP }) .pop() - return res ? - // get next value... - res.__proto__[name] - : undefined } + return !res ? + undefined + :(!(name in res) && typeof(res) == 'function') ? + res + : res[name] } // Find the next parent property descriptor in the prototype chain... @@ -368,6 +398,8 @@ function(proto, name){ // or: // parent(method, this).call(this, ...) // NOTE: for more docs see parent(..) +// +// XXX in the call case need to skip the wrapper function... (???) var parentCall = module.parentCall = function(proto, name, that, ...args){ @@ -575,7 +607,7 @@ function(context, constructor, ...args){ var _mirror_doc = function(func, target){ Object.defineProperty(func, 'toString', { value: function(...args){ - var f = typeof(constructor.prototype) == 'function' ? + var f = typeof(target.prototype) == 'function' ? target.prototype : target.prototype.__call__ return typeof(f) == 'function' ? @@ -603,8 +635,13 @@ function(context, constructor, ...args){ return ( // .prototype is a function... typeof(constructor.prototype) == 'function' ? - constructor.prototype - .call(obj, this, ...arguments) + // 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__(..) : constructor.prototype.__call__ .call(obj, this, ...arguments)) }, @@ -779,9 +816,21 @@ function Constructor(name, a, b, c){ // 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 - && proto.__proto__ === Object.prototype - && (proto.__proto__ = constructor_proto.prototype) + // 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){ @@ -802,15 +851,13 @@ function Constructor(name, a, b, c){ constructor_mixin = mixinFlat({}, constructor_mixin) delete constructor_mixin.__extends__ } !!constructor_proto - && (proto.__proto__ = constructor_proto.prototype) - } - + && (proto.__proto__ = constructor_proto.prototype) } // the constructor base... var _constructor = function Constructor(){ // create raw instance... var obj = _constructor.__rawinstance__ ? - _constructor.__rawinstance__(this, ...arguments) + _constructor.__rawinstance__(this, ...arguments) : RawInstance(this, _constructor, ...arguments) // initialize... obj.__init__ instanceof Function @@ -829,8 +876,8 @@ function Constructor(name, a, b, c){ // set .toString(..)... // NOTE: do this only if .toString(..) is not defined by user... // XXX revise this test... - ;((constructor_mixin || {}).toString === Function.toString - || (constructor_mixin || {}).toString === ({}).toString) + ;((constructor_mixin || {}).toString === Function.prototype.toString + || (constructor_mixin || {}).toString === Object.prototype.toString) && Object.defineProperty(_constructor, 'toString', { value: function(){ var args = proto.__init__ ? diff --git a/package.json b/package.json index e728d56..85ac49b 100755 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ig-object", - "version": "5.0.0", + "version": "5.0.1", "description": "", "main": "object.js", "scripts": { diff --git a/test.js b/test.js index c50a9d3..dac1e0f 100755 --- a/test.js +++ b/test.js @@ -12,6 +12,12 @@ var object = require('./object') +//--------------------------------------------------------------------- + +module.VERBOSE = false + + + //--------------------------------------------------------------------- // helpers... @@ -27,59 +33,125 @@ var instances = function(obj){ return k[0] == k[0].toLowerCase() && o.constructor }) } +var assert = function(pre, stats){ + return function(e, msg, ...args){ + stats + && (stats.assertions += 1) + && !e + && (stats.failures += 1) + module.VERBOSE + && console.log(pre +': '+ msg, ...args) + console.assert(e, pre +': '+ msg, ...args) + return e } } + //--------------------------------------------------------------------- var setups = { - basic: function(msg){ + basic: function(assert){ var X, Y, A, B, C return { - X: X = object.Constructor('A'), - Y: Y = object.C('Y', { }), + X: X = assert(object.Constructor('A'), `Constructor`), + Y: Y = assert(object.C('Y', { }), ` C`), - A: A = object.C('A', Y, { }), - B: B = object.C('B', A, { }), - C: C = object.C('C', B, { }), + A: A = assert(object.C('A', Y, { }), `inherit (gen1)`), + B: B = assert(object.C('B', A, { }), `inherit (gen2)`), + C: C = assert(object.C('C', B, { }), `inherit (gen3)`), } }, - init: function(msg){ + init: function(assert){ return { } }, - call: function(msg){ + call: function(assert){ + var A, B, C, D, F, G + return { + A: A = assert(object.C('A', + function(){ + // XXX + }), 'callable'), + B: B = assert(object.C('B', { + __call__: function(){ + // XXX + }, + }), '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(){ + // XXX how do we get the parent callable??? + object.parent(this) + }), 'call parent'), + F: F = assert(object.C('F', B, { + __call__: function(){ + object.parentCall(F.__call__, this) + }, + }), 'call parent\'s .__call__'), + + + + } }, + native: function(assert){ return { } }, - native: function(msg){ + mixin: function(assert){ return { } }, - instances: function(msg){ + instances: function(assert){ // XXX generate using tests.instance* + // XXX need to be able to use different input setups... return {} }, } var modifiers = { - 'as-is': function(msg, setup){ + // default... + 'as-is': function(assert, setup){ return setup } + + // XXX } var tests = { - instance: function(msg, setup, no_new){ + instance: function(assert, setup, mode){ return constructors(setup) .reduce(function(res, [k, O]){ - var o - no_new ? - console.assert(o = res[k.toLowerCase()] = O(), `${msg}: new:`, k) - : console.assert(o = res[k.toLowerCase()] = new O(), `${msg}: new:`, k) - console.assert(o instanceof O, `${msg}: instanceof:`, k) - console.assert(o.constructor === O, `${msg}: constructor:`, k) - console.assert(o.__proto__ === O.prototype, `${msg}: __proto__:`, k) + 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 + && assert(o instanceof O.__proto__, `instanceof-nested:`, k) + assert(o.constructor === O, `.constructor:`, k) + assert(o.__proto__ === O.prototype, `.__proto__:`, k) return res }, {}) }, - instance_no_new: function(msg, setup){ - return this.instance(msg, setup, true) }, + instance_no_new: function(assert, setup){ + return this.instance(assert, setup, 'no_new') }, + instance_raw: function(assert, setup){ + return this.instance(assert, setup, 'raw') }, + + attributes: function(assert, setup){ + return {} + }, + + // XXX + methods: function(assert, setup){ + constructors(setup) + .forEach(function([k, O]){ + Object.keys(O).forEach(function(m){ + typeof(O[m]) == 'function' + && O[m]() }) + }) + return {} + }, } @@ -90,8 +162,18 @@ var cases = { -// XXX need to report stats... +//--------------------------------------------------------------------- + +// XXX need to have two modes: +// - clean +// - reuse test results again... var runner = function(){ + var stats = { + tests: 0, + assertions: 0, + failures: 0, + } + // tests... Object.keys(tests) .forEach(function(t){ @@ -102,15 +184,21 @@ var runner = function(){ Object.keys(setups) .forEach(function(s){ // run the test... - msg =`test:${t}.${s}.${m}` - tests[t](msg, modifiers[m](msg, setups[s](msg))) }) }) }) + stats.tests += 1 + var _assert = assert(`test:${t}.${s}.${m}`, stats) + tests[t](_assert, + modifiers[m](_assert, + setups[s](_assert))) }) }) }) // cases... Object.keys(cases) .forEach(function(c){ - msg = `case:${c}:` - cases[c](msg) - }) -} + stats.tests += 1 + cases[c]( assert(`case:${c}:`, stats) ) }) + + // stats... + console.log('Tests:', stats.tests, + 'Assertions:', stats.assertions, + 'Failures:', stats.failures) } runner()