blob: 5d775d62fa66e736bb2dd7fc924c783822928988 [file] [log] [blame]
// Copyright 2015 The Vanadium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
/**
* @fileoverview Defines an invoker to invoke service methods.
* @private
*/
module.exports = Invoker;
var createSignature = require('../vdl/create-signature');
var isPublicMethod = require('../lib/service-reflection').isPublicMethod;
var verror = require('../gen-vdl/v.io/v23/verror');
var capitalize = require('../vdl/util').capitalize;
var uncapitalize = require('../vdl/util').uncapitalize;
var isCapitalized = require('../vdl/util').isCapitalized;
var format = require('format');
var context = require('../context');
var asyncCall = require('../lib/async-call');
var InspectableFunction = require('../lib/inspectable-function');
// Method signatures for internal methods that are not present in actual
// signatures.
// These signatures are meant to simplify the implementation of invoke
// and may be partial.
var internalMethodSignatures = {
__glob: {
name: '__glob',
outArgs: []
},
__globChildren: {
name: '__globChildren',
outArgs: []
}
};
/**
* Create an invoker.
* @param {Service} service Service object.
* @constructor
* @private
*/
function Invoker(service) {
if (!(this instanceof Invoker)) {
return new Invoker(service);
}
var invoker = this;
invoker._service = service;
invoker._signature = createSignature(service, service._serviceDescription);
invoker._methods = {};
// See comment in src/vdl/reflect-signature.js for..in loop
for (var key in service) { // jshint ignore:line
if (!isPublicMethod(key, service)) {
continue;
}
if (isCapitalized(key)) {
throw new Error('Can\'t export capitalized method ' + key);
}
var capitalizedMethodName = capitalize(key);
var method = service[key];
var inspectableFn = new InspectableFunction(method);
// Check whether the number of args reported by javascript (method.length)
// and the number of args retrieved from fn.toString() are the same.
// This usually differs if the method is a native method.
if (inspectableFn.names.length !== method.length) {
throw new Error('Function "' + key + '" can not be inspected. ' +
'This is usually because it is a native method or bind is used.');
}
invoker._methods[capitalizedMethodName] = {
name: capitalizedMethodName,
fn: inspectableFn
};
}
var fn;
if (typeof service.__glob === 'function') {
fn = new InspectableFunction(service.__glob);
if (fn.filteredNames.length !== 1 ||
fn.names.indexOf('$stream') === -1) {
// TODO(bjornick): Throw a verror of appropriate type.
throw new Error(
'__glob needs to take in a string and be streaming');
}
this._methods.__glob = {
name: '__glob',
fn: fn
};
}
if (typeof service.__globChildren === 'function') {
fn = new InspectableFunction(service.__globChildren);
if (fn.filteredNames.length !== 0 ||
fn.names.indexOf('$stream') === -1 ) {
// TODO(bjornick): Throw a verror of appropriate type.
throw new Error(
'__globChildren needs to take in no args and be streaming');
}
this._methods.__globChildren = {
name: '__globChildren',
fn: fn
};
}
}
Invoker.prototype.hasGlobber = function() {
return this.hasMethod('__glob') || this.hasMethod('__globChildren');
};
/**
* Find a method signature corresponding to the named method.
*
* @param {String} methodName - The name of the method
* @return {MethodSignature} The signature of the named method, or null.
* @private
*/
Invoker.prototype._findMethodSignature = function(methodName) {
for (var i = 0; i < this._signature.length; i++) {
var sig = this._signature[i];
if (sig.methods) {
for (var m = 0; m < sig.methods.length; m++) {
var method = sig.methods[m];
if (method.name === methodName) {
return method;
}
}
}
}
return null;
};
/**
* Invoker.prototype.invoke - Invoke a method
*
* @param {String} name - The upper camel case name of the method to invoke.
* @param {Array} args - A list of arguments to call the method with, may
* differ because of injections e.g. function x(a,$stream,b) => [0, 2].
* @param {Object} injections - A map of injections, should always
* contain `context`, could also contain `stream`
* e.g. function(ctx, x, $stream, b)
* @param {Invoker~invokeCallback} cb - The callback fired after completion.
*/
Invoker.prototype.invoke = function(name, args, injections, cb) {
// TODO(jasoncampbell): Maybe throw if there are unkown injections
var message;
var err;
var invoker = this;
var method = invoker._methods[name];
var errorContext = injections.context || new context.Context();
if (!method) {
message = format('Method "%s"', name);
err = new verror.NoExistError(errorContext, message);
cb(err);
return;
}
var methodSig = this._findMethodSignature(name) ||
internalMethodSignatures[name];
if (!methodSig) {
cb(verror.InternalError(errorContext,
'Missing method signature for method ' + name));
}
if (!injections.context) {
message = 'Can not call invoker.invoke(...) without a context injection';
err = verror.InternalError(errorContext, message);
cb(err);
return;
}
var arity = method.fn.arity();
// Check argument arity against the method's declared arity
if (args.length !== arity) {
var template = 'Expected %d arguments but got "%s"';
message = format(template, arity, args.join(', '));
err = new verror.BadArgError(errorContext, message);
cb(err);
return;
}
// Clone the array so we can simply manipulate and apply later
var clonedArgs = args.slice(0);
// call and context go in front
clonedArgs.unshift(injections.call);
clonedArgs.unshift(injections.context);
// splice in stream
if (injections.stream) {
var start = method.fn.position('$stream');
var deleteCount = 0;
clonedArgs.splice(start, deleteCount, injections.stream);
}
asyncCall(injections.context, invoker._service, method.fn,
methodSig.outArgs.map(function(outArg) {
var name = outArg.name;
return name ? uncapitalize(name) : '_';
}), clonedArgs, cb);
};
/**
* This callback is fired on completion of invoker.invoke.
* @callback Invoker~invokeCallback
* @param {Error} err
* @param {results} results
*/
/**
* Return the signature of the service.
* @return {Object} The signature
*/
Invoker.prototype.signature = function() {
return this._signature;
};
/**
* returns whether the function <name> is invokable.
* @param {string} name the name of the function
* @return {boolean} whether the function is invokable.
*/
Invoker.prototype.hasMethod = function(name) {
return !!this._methods[name];
};