I'm working on an API client that allows to call specific API methods when providing the ID of a foo
, like so:
apiClient.myApiMethod('myFooId', 'firstApiArg', 'nthApiArg');
For developer convenience, I'm trying to implement custom proxy objects:
var myFoo = apiClient.registerFoo('myFoo', 'myFooId');
myFoo.myApiMethod('firstApiArg', 'nthApiArg');
After searching for a while, I figured ES6 proxies may be best suited for that, as the fooId
needs to be inserted as the first argument of the method call to support both ways of working.
Therefore, I created the following code. If an object property of Foo.myFoos
is called (eg. Foo.myFoos.example
), its is searched for in _myFooItems
, and if it exists there, another Proxy object is returned.
Now if a method is called on that object, it is searched for in the properties of Foo
, and if found, the Foo
method is called with myFooId
as its first argument.
That means, you should be able to Foo.myFoos.example.parentMethodX('bar', 'baz')
.
var Foo = function() {
// parent instance
_self = this;
// custom elements dictionary
_myFooItems = {};
// to call parent methods directly on custom elements
this.myFoos = Object.create(new Proxy({}, {
// property getter function (proxy target and called property name as params)
get: function(target, myFooName) {
// whether called property is a registered foo
if (_myFooItems.hasOwnProperty(myFooName)) {
// create another proxy to intercept method calls on previous one
return Object.create(new Proxy({}, {
// property getter function (proxy target and called property name as params)
get: function(target, methodName) {
// whether parent method exists
if (_self.hasOwnProperty(methodName)) {
return function(/* arguments */) {
// insert custom element ID into args array
var args = Array.prototype.slice.call(arguments);
args.unshift(_myFooItems[ myFooName ]);
// apply parent method with modified args array
return _self[ methodName ].apply(_self, args);
};
} else {
// parent method does not exist
return function() {
throw new Error('The method ' + methodName + ' is not implemented.');
}
}
}
}
));
}
}
}
));
// register a custom foo and its ID
this.registerFoo = function(myFooName, id) {
// whether the foo has already been registered
if (_myFooItems.hasOwnProperty(myFooName)) {
throw new Error('The Foo ' + myFooName + ' is already registered in this instance.');
}
// register the foo
_myFooItems[ myFooName ] = id;
// return the created foo for further use
return this.myFoos[ myFooName ];
};
};
module.exports = Foo;
Though what happens if you run the code and try to register a foo
(the above code is working as is in Node>=6.2.0), is that the following Error is thrown:
> var exampleFoo = Foo.registerFoo('exampleFoo', 123456)
Error: The method inspect is not implemented.
at null.<anonymous> (/path/to/module/nestedProxyTest.js:40:31)
at formatValue (util.js:297:21)
at Object.inspect (util.js:147:10)
at REPLServer.self.writer (repl.js:366:19)
at finish (repl.js:487:38)
at REPLServer.defaultEval (repl.js:293:5)
at bound (domain.js:280:14)
at REPLServer.runBound [as eval] (domain.js:293:12)
at REPLServer.<anonymous> (repl.js:441:10)
at emitOne (events.js:101:20)
After spending way to much time thinking about why the second proxy even tries to call a method if none is given to it, I eventually gave up. I'd expect exampleFoo
to be a Proxy object that accepts Foo
methods if called.
What causes the actual behaviour here?
I don't think you should use proxies here at all. Assuming you have that API with a monstrous
class Foo {
…
myApiMethod(id, …) { … }
… // and so on
}
then the cleanest way to achieve what you are looking for is
const cache = new WeakMap();
Foo.prototype.register = function(id) {
if (!cache.has(this))
cache.set(this, new Map());
const thisCache = cache.get(this);
if (!thisCache.get(id))
thisCache.set(id, new IdentifiedFoo(this, id));
return thisCache.get(id);
};
class IdentifiedFoo {
constructor(foo, id) {
this.foo = foo;
this.id = id;
}
}
Object.getOwnPropertyNames(Foo.prototype).forEach(function(m) {
if (typeof Foo.prototype[m] != "function" || m == "register") // etc
return;
IdentifiedFoo.prototype[m] = function(...args) {
return this.foo[m](this.id, ...args);
};
});
so that you can do
var foo = new Foo();
foo.myApiMethod(id, …);
foo.register(id).myApiMethod(…);