Source: index.js


'use strict';

var mkFn = require('mk-fn');
var extend = require('extend');
var path = require('path');
var getParams = require('get-parameter-names');

/** @module procnet */
var procnet = {};

/** 
 * More like syntatic sugar for defining services. You can list your dependencies
 * array to query them later.
 * <code><pre>
 * var foo = procnet.service(['bar'], function(bar) {
 *   return {
 *     foobar: function() { return 'foo' + bar; }
 *   };
 * });
 * procnet.dependencies(foo); // => ['bar']
 * var instance = foo('bar');
 * instance.foobar(); // => 'foobar'
 * </pre></code>
 *
 * You also have the option of omitting the array and listing your dependencies
 * directly just like with angular modules. It will infer what you mean
 * automatically.
 * <code><pre>
 * var rectangle = procnet.injectable(function(math) {
 *   return {
 *     perimeter: function(h, w) {
 *       return math.add(math.double(h), math.double(w));
 *     }
 *   };
 * });
 * procnet.dependencies(rectangle); // => ['math']
 * </pre></code>

 * @function service
 * @static
 * @param {array} dependencies Is an array of strings where each string is the
 * name of the service that the service requires to run.
 * @param {function} factory Is the funciton which returns the list of 
 * procedures. 
 * @returns function
 */
procnet.service = function service (dependencies, factory) {	
	var instantiator = arguments.length > 1 ? arguments[1] : arguments[0];		
	// This is NOT part of the public API and thus can be changed at any time.
	instantiator._dependencies = arguments.length > 1 ? arguments[0] : getParams(instantiator);

	instantiator['@@service'] = true;
	return instantiator;	
};

/**
 * @function injectable
 * @static
 * @alias service
 */
procnet.injectable = procnet.service;

/**
 * Returns the dependencies that need to be resoled and passed to the factory
 * to create a service instance.
 * @function dependencies
 * @static
 * @param {object} serviceFactory The factory which is used to create the
 * service;
 * @returns array
 */
procnet.dependencies = function dependencies(serviceFactory) {
	return serviceFactory._dependencies;
};

/** 
 * Takes the configuration with the type and then the name and changes it into 
 * a object with the name of the services as the key.
 *
 * @private
 * @function _flattenConfig
 * @static
 * @param {object} config Is the user-specified configuration to be flattened.
 * @returns object
 */
procnet._flattenConfig = function _flattenConfig(config) {
	var res = {};
	for(var type in config) {
		var typeSpace = config[type];
		for(var name in typeSpace) {
			var nameSpace = typeSpace[name];
			// private property, might be changed without major version bump.
			nameSpace._serviceType = type;
			res[name] = nameSpace;
		}
	}
	return res;
};

/** 
 * Load all dependencies for the user to create the service. 
 *
 * @private
 * @static
 * @function _loadServices
 * @param {object} factories Is a promise factory.
 * @param {object} config Is the flattened configuration file for all 
 * dependencies.
 * @param {array} dependencies Is an array of the dependency names.
 * @param {function} cb Complete handler. Either get and error or an array of
 * the remotes in the order given by the dependencies argument.
 * @returns void
 */
procnet._loadServices = function _loadServices(factories, config, dependencies, cb) {
	var remotes = [];
	function reduce(i) {
		if(remotes.length >= dependencies.length) {
			cb(null, remotes);
		} else {
			var depName = dependencies[i];
			var remoteConf = config[depName];
			var type = remoteConf._serviceType;
			factories[type](remoteConf, function(err, remote) {
				if(err) return cb(err);
				remotes.push(remote);
				reduce(i + 1);
			});
		}
	}
	reduce(0);
};

/** 
 * Asynchronous dependency loader for setting up services. This will load up 
 * the service and return the precedures object that the service generates.
 *
 * @static
 * @function loader
 * @param {object} factories Is a map of service names which translate to a 
 * function accepting an option and a callback.
 * @param {object} config The configuration is used to store data which could 
 * be server-specific as well as other things such as ip addresses. Each 
 * factory is given the corresponding configuration needed to create the 
 * instance it is being asked to generate.
 * @returns function
 */
procnet.loader = function loader(factories, config) {
	// start by flattening the config since I dont need to know
	// more information to do that
	var flatConfig = procnet._flattenConfig(config);

	return function(service, cb) {
		// load services
		function whenLoaded(err, remotes) {
			if(err) return cb(err);
			var procs = service.apply(null, remotes);
			cb(null, procs);
		}
		procnet._loadServices(factories, flatConfig, service._dependencies, whenLoaded);
	};
};

/** 
 * A client is nothing more than a consumer. A good example would be a  server 
 * with a REST api trying to call remote services using procnet. It could of 
 * course also be a browser client trying to fetch data from remote servers.
 *
 * @static
 * @function client
 * @param {object} factories Contains all of the instanciators for the service 
 * types.
 * @param {object} config Has the configuration for each of the services.
 * @see loader
 */ 
procnet.client = function client(factories, config) {
	var flatConfig = procnet._flattenConfig(config);
	return function(deps, cb) {
		procnet._loadServices(factories, flatConfig, deps, function(err, remotes) {
			if(err) return cb(err);

			var remoteObj = deps.reduce(function(obj, name, i) {
				obj[name] = remotes[i];
				return obj;
			}, {});
			cb(null, remoteObj);

		});
	};
};

/** 
 * Utility function for mocking service procedures for unit testing. It only 
 * makes the function always returna promise, which generated services don't 
 * always do.
 *
 * @static
 * @function mockFn
 * @param {function} promise Is a promise factory.
 * @param {function} fn Is the function to mock.
 * @returns function
 */
procnet.mockFn = function mockFn(promise, fn) {
	return mkFn(fn.length, function() {
		var args = arguments;
		var t = this;
		return promise(function(resolve, reject) {
			resolve(fn.apply(t, args));
		});
	});
};

/**
 * Function which will return each value specified in the array in sequence
 * for each call. Each value will be promisified.
 *
 * <code><pre>
 * var values = [1, 2];
 *
 * var roller = procnet.rollingFn(promise, values);
 *
 * roller('foobar?'); // -> returns a resolved promise with the value 1.
 * roller('foobaz!'); // -> returns a resolved promise with the value 2.
 * roller('fooboo');  // -> returns a resolved promise with the value undefined.
 *
 * </pre></code>
 * 
 * This is useful for mocking functions such as database access functions.
 *
 * @static
 * @function rollingFn
 * @param {function} promise Is a promise factory.
 * @param {array} values Are the values the function will return on each
 * subsequent call.
 * @param {number} ln Is the length property of the function. This parameter is
 * optional.
 */ 
procnet.rollingFn = function rollingFn(promise, values, ln) {
	var iterator = 0;
	var fn = function() {
		return promise(function(resolve, reject) {
			resolve(values[iterator++]);
		});
	};

	if(ln !== undefined) return mkFn(ln, fn);
	else return fn;
};

/** 
 * Takes in a object with functions and ensures that they always return a 
 * promise. Useful for injecting them into services as fake networked remotes.
 *
 * @static
 * @function mockRemote
 * @param {function} promise is a promise factory.
 * @param {object} mock is the set of functions to mock. Can also contain 
 * objects, where the functons inside of that object will be mocked.
 * @returns object
 */ 
procnet.mockRemote = function mockRemote(promise, mock) {
	return Object.keys(mock).reduce(function(obj, k) {
		// this should be able to handle deep objects.
		if(typeof mock[k] === 'function')
			obj[k] = procnet.mockFn(promise, mock[k]);
		else
			obj[k] = procnet.mockRemote(promise, mock[k]);
		return obj;
	}, {});
};

/** 
 * To unit test, one will need to use mocking due to the services having 
 * external dependencies. Since procnet is so simple, you can still call 
 * directly the procedures once the service is instantianted.
 *
 * <code><pre>
 * var rectangle = procnet.service(['math'], function(math) {
 *   return {
 *     surface: function(a, b) {
 *       return math.multiply(a, b);
 *     }
 *   };
 * });
 *
 * var mock = procnet.mocker(promiseFactory);
 *
 * var mocked = mock({
 *   math: {
 *     multiply: function(a, b) { return a + b; }
 *   }
 * }, rectangle);
 *
 * mocked
 *   .surface(2, 5)
 *   .then(function(r) {
 *     assert.equal(r, 7);
 *   });
 * </pre></code>
 * @static
 * @function mocker
 * @param {function} promise is the promise factory
 * @param {object} mocks is an object where the key is the name of the service.
 * @param {function} service is the function returning an object will all of 
 * the procedures.
 * @returns function
 */
procnet.mocker = function mocker(promise) {
	return function(mocks, service) {
		var mocked = service._dependencies.map(function(depName) {
			var dep = mocks[depName];
			return procnet.mockRemote(promise, mocks[depName]);			
		});

		return procnet.mockRemote(promise, service.apply(null, mocked));
	};
};

/**
 * This allows you to more or less do integration testing while still avoiding
 * to make network calls.
 *
 * <code><pre>
 *
 * var leafs = {
 *   postgres: function() {},
 *   math: procnet.mockRemote(math)
 * };
 *
 * var recangle = procnet.service(['math'], function(math) {
 *   return {
 *     surface: function(w, h) {
 *       return math.multiply(w, w);
 *     }
 *   };
 * });
 *
 * var services = procnet.mockCluster(leafs, { rectangle: rectangle });
 *
 * services.rectangle.surface(10, 2).then(function(res) {
 *   assert.equal(res, 20);
 * });
 * </pre></code>
 * @static
 * @function mockCluster
 * @returns object
 * @param {function} promise Is yet again, a promise factory.
 * @param {object} leafs These are already loaded dependencies. For example,
 * if you still want to have a real database connection, you can inject it into
 * the cluster by adding its name and reference to the object.
 * @param {object} branches Are services which need to be mocked.
 */ 
procnet.mockCluster = function mockCluster(promise, leafs, branches) {
	
	function mock(loaded, load) {

		function isLoaded (serviceName) {
			return loaded[serviceName] !== undefined;
		}
		
		function mapService(serviceName) {
			return loaded[serviceName];
		}

		for(var serviceName in load) {
			var service = load[serviceName];
			// if I can load the service with all of its requirements, do it!
			var canLoad = service && service._dependencies.every(isLoaded);
			if(canLoad) {
				var deps = service._dependencies.map(mapService);
				var loadedService = service.apply(null, deps);
				loaded[serviceName] = procnet.mockRemote(promise, loadedService);
				load[serviceName] = undefined;
				return mock(loaded, load);
			}
		}

		if(Object.keys(load) > 0) throw 'Could not load cluster';
		return loaded;
	}

	return mock(extend({}, leafs), extend({}, branches));
};

/**
 * Returns a tuple where the first item is all the services in the object, and the rest are not.
 * This is a partition operation.
 *
 * @static
 * @function
 * @private
 */
procnet._serviceInObj = function(obj) {
	return Object.keys(obj).reduce(function(accu, depName) {
		var ind = obj[depName]['@@service'] ? 0 : 1;
		accu[ind][depName] = obj[depName]

		return accu;
	}, [{}, {}]);

};

/**
 * Takes the injectable and what has been loaded already and returns what is
 * available to load into the 
 *
 * @function
 * @static
 * @private
 */
procnet._injectables = function(inj, loaded) {
	return inj._dependencies.map(function(depName) {
		return loaded[depName];
	});
};

/**
 * Finds the next item that can be resolved. Returns undefined if there are no items
 * which can be resolved using what is already loaded.
 *
 * @function
 * @static
 * @private
 *
 */
procnet._nextToResolve = function(notLoaded, loaded) {
	var loadedNames = Object.keys(loaded);
	return Object.keys(notLoaded).filter(function(k) {
		var inj = notLoaded[k];

		var deps = inj._dependencies;
		return deps.every(function(depName) {
			return loadedNames.indexOf(depName) > -1;
		});
	})[0];
};

/**
 * Returns true if the value is thenable.
 *
 * @function
 * @static
 * @private
 */
procnet._isAsync = function(val) {
	return typeof val.then === 'function';
};

/**
 * Walks to the next item to resolve. If there are async modules it will return
 * a promise, if not then it will return the resolved modules.
 */
procnet._resolveNext = function(rest, loaded) {
	if(Object.keys(rest).length === 0) {
		return loaded;
	}
	var nextName = procnet._nextToResolve(rest, loaded);
	if(nextName === undefined) {
		throw new Error('Could not resolve dependencies');
	}

	var next = rest[nextName];
	var inject = procnet._injectables(next, loaded);
	var applied = next.apply(null, inject);

	if(procnet._isAsync(applied)) {
		return applied.then(function(ap) {
			var nextRest = extend({}, rest);
			delete nextRest[nextName];

			var nextLoaded = extend({}, loaded);
			nextLoaded[nextName] = ap;

			return procnet._resolveNext(nextRest, nextLoaded);
		});
	} else {
		var nextRest = extend({}, rest);
		delete nextRest[nextName];

		var nextLoaded = extend({}, loaded);
		nextLoaded[nextName] = applied;
		return procnet._resolveNext(nextRest, nextLoaded);
	}

};

/**
 * Similar to loader, but instead is synchronous. More useful if you're just
 * using this library for dependency injection. Its really just synchronous
 * by default instead of forcing all functions to return a promise.
 *
 * <code><pre>
 *
 * var math = procnet.injectable(function() {
 *   return {
 *     add: function(a, b) { return a + b; },
 *     double: function(a) { return a * 2; }
 *   };
 * });
 *
 * var rectangle = procnet.injectable(['math'], function(math) {
 *   return {
 *     perimeter: function(h, w) {
 *       return math.add(math.double(h), math.double(w));
 *     }
 *   };
 * });
 *
 * var services = {
 *   'rectangle': rectangle,
 *   'math': math
 * };
 *
 * var loaded = procnet.resolve(services);
 *
 * console.log(loaded.perimeter(1, 1)); // => 4
 * </pre></code>
 *
 *
 * This function also supports async dependency loading, where an injectable
 * returns a promise instead of a instance. In this case the function will
 * return a promise. This is useful for injectables which need to load data
 * from databases and such.
 *
 * <code><pre>
 * var numberCache = procnet.injectable(function() {
 *   return Promise.resolve({
 *     1: 'One',
 *     2: 'Two',
 *     3: 'Three'
 *   });
 * });
 * var add = procnet.injectable(function(numberCache) {
 *   return function(x, y) { return x + y; };
 * });
 *
 * var loadedAsync = procnet.resolve({
 *   numberCache: numberCache,
 *   add: add
 * });
 * loadedAsync.then(function(loaded) {
 *   loaded.add(1, 2); // => 'Three'
 * });
 * </pre></code>
 *
 * @function resolve
 * @static
 * @param {object} toResolve Is the list of services that are either already
 * loaded or need to be loaded.
 * @returns {object | promise} All the services loaded. If some of the service instances are
 * returned as promises then the object returned will also be a promise.
 */

procnet.resolve = function(toResolve) {
	var result = procnet._serviceInObj(toResolve);
	var rest = result[0];
	var loaded = result[1];

	return procnet._resolveNext(rest, loaded);
};

module.exports = procnet;