reworked access to parent callable implementations...

Signed-off-by: Alex A. Naanou <alex.nanou@gmail.com>
This commit is contained in:
Alex A. Naanou 2020-05-24 23:59:01 +03:00
parent 1b5d27afc1
commit a5bab42336
4 changed files with 252 additions and 57 deletions

View File

@ -213,9 +213,16 @@ var Base = object.Constructor('Base', {
}) })
var Item = object.Constructor('Item', Base, { var Item = object.Constructor('Item', Base, {
method: function(){
// ...
// call the "super" method...
return object.parentCall(Item.prototype, 'method', this, ...arguments)
},
__init__: function(){ __init__: function(){
// call the "super" method... // call the "super" method...
object.parentCall(this.prototype.__init__, this) object.parentCall(this.__init__, this, ...arguments)
this.item_attr = 'instance attribute value' this.item_attr = 'instance attribute value'
}, },
@ -245,7 +252,8 @@ var Action = object.Constructor('Action',
var Action2 = object.Constructor('Action2', { var Action2 = object.Constructor('Action2', {
__call__: function(context, ...args){ __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 If the prototype is explicitly defined as a function then it is the
user's responsibility to call `.__call__(..)` method. 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:** **Notes:**
- the two approaches (_function_ vs. `.__call__(..)`) will produce - the two approaches (_function_ vs. `.__call__(..)`) will produce
@ -497,6 +516,18 @@ one of the following:
`callback(..)` and continue. `callback(..)` and continue.
Special case: get callable implementations
```
sources(<object>, '__call__')
sources(<object>, '__call__', <callback>)
-> <list>
```
This will get the callable implementations regardless of the actual
implementation details, i.e. both function prototype or `.__call__(..)`
methods will be matched.
### `values(..)` ### `values(..)`
Get values for attribute in prototype chain Get values for attribute in prototype chain
@ -528,7 +559,7 @@ callback(<descriptor>, <source>)
-> <value> -> <value>
``` ```
See [`sources(..)`](#sources) for docs on `callback(..)` See [`sources(..)`](#sources) for docs on `callback(..)` and special cases.
### `parent(..)` ### `parent(..)`
@ -537,18 +568,37 @@ Get parent attribute value or method
parent(<prototype>, <name>) parent(<prototype>, <name>)
-> <parent-value> -> <parent-value>
-> undefined -> undefined
```
It is recommended to use the relative`<constructor>.prototype` as
`<prototype>` 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(<method>, <this>) parent(<method>, <this>)
-> <parent-method> -> <parent-method>
-> undefined -> undefined
``` ```
_Edge case: The `parent(<method>, ..)` has one potential pitfall -- in _Edge case: The `parent(<method>, ..)` has one potential pitfall -- in
the rare case where a prototype chain contains two or more references the rare case where a prototype chain contains two or more references
to the same method under the same name, `parent(..)` can't distinguish to the same method under the same name, `parent(..)` can't distinguish
between these references and will always return the second one._ between these references and will always return the second one._
Special case: get the parent callable implementation
```
parent(<prototype>, '__call__')
-> <parent-value>
-> undefined
```
See [`sources(..)`](#sources) for more info on the special case.
### `parentProperty(..)` ### `parentProperty(..)`
@ -573,6 +623,15 @@ parentCall(<method>, <this>)
-> undefined -> undefined
``` ```
Special case: call the parent callable implementation
```
parentCall(<prototype>, '__call__', <this>)
-> <result>
-> undefined
```
See [`parent(..)`](#parent) and [`sources(..)`](#sources) for more details.
### `mixin(..)` ### `mixin(..)`
@ -814,4 +873,5 @@ Still, this is worth some thought.
Copyright (c) 2019, Alex A. Naanou, Copyright (c) 2019, Alex A. Naanou,
All rights reserved. All rights reserved.
<!-- vim:set ts=4 sw=4 spell : --> <!-- vim:set ts=4 sw=4 spell : -->

View File

@ -181,13 +181,18 @@ module.STOP =
// for any overloading in the instance, though this approach is // for any overloading in the instance, though this approach is
// not very reusable.... // not very reusable....
// NOTE: this will not trigger any props... // 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 = var sources =
module.sources = module.sources =
function(obj, name, callback){ function(obj, name, callback){
var o var o
var res = [] var res = []
while(obj != null){ while(obj != null){
if(obj.hasOwnProperty(name)){ //if(obj.hasOwnProperty(name)){
if(obj.hasOwnProperty(name)
|| (name == '__call__' && typeof(obj) == 'function')){
// handle callback... // handle callback...
o = callback o = callback
&& callback(obj) && callback(obj)
@ -223,27 +228,33 @@ function(obj, name, callback){
// //
// //
// NOTE: for more docs on the callback(..) see sources(..) // NOTE: for more docs on the callback(..) see sources(..)
//
// XXX document the '__call__' cpecial case...
var values = var values =
module.values = module.values =
function(obj, name, callback, props){ function(obj, name, callback, props){
props = callback === true ? props = callback === true ?
callback callback
: props : 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... // wrap the callback if given...
var c = typeof(callback) == 'function' var c = typeof(callback) == 'function'
&& function(obj){ && function(obj){
return callback( return callback(_get(obj, name), obj) }
props ?
Object.getOwnPropertyDescriptor(obj, name)
: [ obj[name] ],
obj) }
return sources(...(c ? return sources(...(c ?
[obj, name, c] [obj, name, c]
: [obj, name])) : [obj, name]))
.map(function(obj){ .map(function(obj){
return props ? return _get(obj, name) }) }
Object.getOwnPropertyDescriptor(obj, name)
: obj[name] }) }
// Find the next parent attribute in the prototype chain. // Find the next parent attribute in the prototype chain.
@ -258,6 +269,10 @@ function(obj, name, callback, props){
// -> meth // -> meth
// -> undefined // -> undefined
// //
// Get parent object...
// parent(this)
// -> parent
//
// //
// The two forms differ in: // The two forms differ in:
// - in parent(method, ..) a method's .name attr is used for name. // - 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. // and to the method after the match.
// NOTE: this is super(..) replacement, usable in any context without // NOTE: this is super(..) replacement, usable in any context without
// restriction -- super(..) is restricted to class methods only... // 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 = var parent =
module.parent = module.parent =
function(proto, name){ function(proto, name){
// special case: get parent...
if(arguments.length == 1){
return proto.__proto__ }
// special case: get method... // special case: get method...
if(typeof(name) != typeof('str')){ if(typeof(name) != typeof('str')){
var that = name var that = name
@ -317,13 +343,17 @@ function(proto, name){
&& module.STOP }) && module.STOP })
.pop() } .pop() }
// get first source... // get first source...
var c = 0
var res = sources(proto, name, var res = sources(proto, name,
function(obj){ return module.STOP }) function(obj){
return c++ == 1
&& module.STOP })
.pop() .pop()
return res ? return !res ?
// get next value... undefined
res.__proto__[name] :(!(name in res) && typeof(res) == 'function') ?
: undefined } res
: res[name] }
// Find the next parent property descriptor in the prototype chain... // Find the next parent property descriptor in the prototype chain...
@ -368,6 +398,8 @@ function(proto, name){
// or: // or:
// parent(method, this).call(this, ...) // parent(method, this).call(this, ...)
// NOTE: for more docs see parent(..) // NOTE: for more docs see parent(..)
//
// XXX in the call case need to skip the wrapper function... (???)
var parentCall = var parentCall =
module.parentCall = module.parentCall =
function(proto, name, that, ...args){ function(proto, name, that, ...args){
@ -575,7 +607,7 @@ function(context, constructor, ...args){
var _mirror_doc = function(func, target){ var _mirror_doc = function(func, target){
Object.defineProperty(func, 'toString', { Object.defineProperty(func, 'toString', {
value: function(...args){ value: function(...args){
var f = typeof(constructor.prototype) == 'function' ? var f = typeof(target.prototype) == 'function' ?
target.prototype target.prototype
: target.prototype.__call__ : target.prototype.__call__
return typeof(f) == 'function' ? return typeof(f) == 'function' ?
@ -603,8 +635,13 @@ function(context, constructor, ...args){
return ( return (
// .prototype is a function... // .prototype is a function...
typeof(constructor.prototype) == 'function' ? typeof(constructor.prototype) == 'function' ?
constructor.prototype // NOTE: we are not using .call(..) here as it
.call(obj, this, ...arguments) // 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__(..) // .__call__(..)
: constructor.prototype.__call__ : constructor.prototype.__call__
.call(obj, this, ...arguments)) }, .call(obj, this, ...arguments)) },
@ -779,9 +816,21 @@ function Constructor(name, a, b, c){
// handle: // handle:
// Constructor(name, constructor, ..) // 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 constructor_proto
&& proto.__proto__ === Object.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) && (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__ // handle: .__extends__
if(!constructor_proto){ if(!constructor_proto){
@ -802,9 +851,7 @@ function Constructor(name, a, b, c){
constructor_mixin = mixinFlat({}, constructor_mixin) constructor_mixin = mixinFlat({}, constructor_mixin)
delete constructor_mixin.__extends__ } delete constructor_mixin.__extends__ }
!!constructor_proto !!constructor_proto
&& (proto.__proto__ = constructor_proto.prototype) && (proto.__proto__ = constructor_proto.prototype) }
}
// the constructor base... // the constructor base...
var _constructor = function Constructor(){ var _constructor = function Constructor(){
@ -829,8 +876,8 @@ function Constructor(name, a, b, c){
// set .toString(..)... // set .toString(..)...
// NOTE: do this only if .toString(..) is not defined by user... // NOTE: do this only if .toString(..) is not defined by user...
// XXX revise this test... // XXX revise this test...
;((constructor_mixin || {}).toString === Function.toString ;((constructor_mixin || {}).toString === Function.prototype.toString
|| (constructor_mixin || {}).toString === ({}).toString) || (constructor_mixin || {}).toString === Object.prototype.toString)
&& Object.defineProperty(_constructor, 'toString', { && Object.defineProperty(_constructor, 'toString', {
value: function(){ value: function(){
var args = proto.__init__ ? var args = proto.__init__ ?

View File

@ -1,6 +1,6 @@
{ {
"name": "ig-object", "name": "ig-object",
"version": "5.0.0", "version": "5.0.1",
"description": "", "description": "",
"main": "object.js", "main": "object.js",
"scripts": { "scripts": {

144
test.js
View File

@ -12,6 +12,12 @@ var object = require('./object')
//---------------------------------------------------------------------
module.VERBOSE = false
//--------------------------------------------------------------------- //---------------------------------------------------------------------
// helpers... // helpers...
@ -27,59 +33,125 @@ var instances = function(obj){
return k[0] == k[0].toLowerCase() && o.constructor }) } 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 = { var setups = {
basic: function(msg){ basic: function(assert){
var X, Y, A, B, C var X, Y, A, B, C
return { return {
X: X = object.Constructor('A'), X: X = assert(object.Constructor('A'), `Constructor`),
Y: Y = object.C('Y', { }), Y: Y = assert(object.C('Y', { }), ` C`),
A: A = object.C('A', Y, { }), A: A = assert(object.C('A', Y, { }), `inherit (gen1)`),
B: B = object.C('B', A, { }), B: B = assert(object.C('B', A, { }), `inherit (gen2)`),
C: C = object.C('C', B, { }), C: C = assert(object.C('C', B, { }), `inherit (gen3)`),
} }, } },
init: function(msg){ init: function(assert){
return { 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 { return {
} }, } },
native: function(msg){ mixin: function(assert){
return { return {
} }, } },
instances: function(msg){ instances: function(assert){
// XXX generate using tests.instance* // XXX generate using tests.instance*
// XXX need to be able to use different input setups...
return {} return {}
}, },
} }
var modifiers = { var modifiers = {
'as-is': function(msg, setup){ // default...
'as-is': function(assert, setup){
return setup } return setup }
// XXX
} }
var tests = { var tests = {
instance: function(msg, setup, no_new){ instance: function(assert, setup, mode){
return constructors(setup) return constructors(setup)
.reduce(function(res, [k, O]){ .reduce(function(res, [k, O]){
var o var o = res[k.toLowerCase()] =
no_new ? mode == 'no_new' ?
console.assert(o = res[k.toLowerCase()] = O(), `${msg}: new:`, k) assert(O(), `new:`, k)
: console.assert(o = res[k.toLowerCase()] = new O(), `${msg}: new:`, k) : mode == 'raw' ?
console.assert(o instanceof O, `${msg}: instanceof:`, k) assert(O.__rawinstance__(), `.__rawinstance__()`, k)
console.assert(o.constructor === O, `${msg}: constructor:`, k) : assert(new O(), `new:`, k)
console.assert(o.__proto__ === O.prototype, `${msg}: __proto__:`, 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 }, {}) }, return res }, {}) },
instance_no_new: function(msg, setup){ instance_no_new: function(assert, setup){
return this.instance(msg, setup, true) }, 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 runner = function(){
var stats = {
tests: 0,
assertions: 0,
failures: 0,
}
// tests... // tests...
Object.keys(tests) Object.keys(tests)
.forEach(function(t){ .forEach(function(t){
@ -102,15 +184,21 @@ var runner = function(){
Object.keys(setups) Object.keys(setups)
.forEach(function(s){ .forEach(function(s){
// run the test... // run the test...
msg =`test:${t}.${s}.${m}` stats.tests += 1
tests[t](msg, modifiers[m](msg, setups[s](msg))) }) }) }) var _assert = assert(`test:${t}.${s}.${m}`, stats)
tests[t](_assert,
modifiers[m](_assert,
setups[s](_assert))) }) }) })
// cases... // cases...
Object.keys(cases) Object.keys(cases)
.forEach(function(c){ .forEach(function(c){
msg = `case:${c}:` stats.tests += 1
cases[c](msg) cases[c]( assert(`case:${c}:`, stats) ) })
})
} // stats...
console.log('Tests:', stats.tests,
'Assertions:', stats.assertions,
'Failures:', stats.failures) }
runner() runner()