Source: index.js

'use strict';

/**
 * Middleware are callbacks that will be executed when a hook is called. The type of middleware is determined through the parameters it declares.
 * @example
 * function(){
 *   //synchronous execution
 * }
 * @example
 * function(){
 *   //create promise, i.e. further execution is halted until the promise is resolved.
 *   return promise;
 * }
 * @example
 * function(next){
 *   //asynchronous execution, i.e. further execution is halted until `next` is called.
 *   setTimeout(next, 1000);
 * }
 * @example
 * function(next, done){
 *   //asynchronous execution, i.e. further execution is halted until `next` is called.
 *   setTimeout(next, 1000);
 *   //full middleware queue handling is halted until `done` is called.
 *   setTimeout(done, 2000);
 * }
 * @callback middleware
 * @param {...*} [parameters] - parameters passed to the hook
 * @param {function} [next] - pass control to the next middleware
 * @param {function} [done] - mark parallel middleware to have completed
 */

/**
 * @typedef {Object} options
 * @property {Boolean} [strict=true] - Will disallow subscribing to middleware bar the explicitly registered ones.
 * @property {Object} [qualifiers]
 * @property {String} [qualifiers.pre='pre'] - Declares the 'pre' qualifier
 * @property {String} [qualifiers.post='post'] - Declares the 'post' qualifier
 * @property {Function} [createThenable=undefined] - Set a Promise A+ compliant factory function for creating promises.
 * @example
 * //creates a GrapplingHook instance with `before` and `after` hooking
 * var instance = grappling.create({
 *   qualifiers: {
 *     pre: 'before',
 *     post: 'after'	
 *   }
 * });
 * instance.before('save', console.log);
 * @example
 * //creates a GrapplingHook instance with a promise factory
 * var P = require('bluebird');
 * var instance = grappling.create({
 *   createThenable: function(fn){
 *     return new P(fn);
 *   }
 * });
 * instance.allowHooks('save');
 * instance.pre('save', console.log);
 * instance.callThenableHook('pre:save', 'Here we go!').then(function(){
 *   console.log('And finish!');
 * });
 * //outputs:
 * //Here we go!
 * //And finish!
 */

/**
 * The GrapplingHook documentation uses the term "thenable" instead of "promise", since what we need here is not _necessarily_ a promise, but a thenable, as defined in the <a href="https://promisesaplus.com/">Promises A+ spec</a>.
 * Thenable middleware for instance can be _any_ object that has a `then` function.
 * Strictly speaking the only instance where we adhere to the full Promises A+ definition of a promise is in {@link options}.createThenable. 
 * For reasons of clarity, uniformity and symmetry we chose `createThenable`, although strictly speaking it should've been `createPromise`.
 * Most people would find it confusing if part of the API uses 'thenable' and another part 'promise'.
 * @typedef {Object} thenable
 * @property {Function} then - see <a href="https://promisesaplus.com/">Promises A+ spec</a>
 * @see {@link options}.createThenable
 * @see {@link module:grappling-hook.isThenable isThenable}
 */

var _ = require('lodash');
var async = require('async');

var presets = {};

function parseHook(hook) {
	var parsed = (hook) ? hook.split(':') : [];
	var n = parsed.length;
	return {
		type: parsed[n - 2],
		name: parsed[n - 1]
	};
}

/**
 *
 * @param instance grappling-hook instance
 * @param hookType qualifier
 * @param hookName action
 * @param args
 * @private
 */
function addMiddleware(instance, hook, args) {
	var fns = _.flatten(args);
	var cache = instance.__grappling;
	var mw = [];
	if (!cache.middleware[hook]) {
		if (cache.opts.strict) throw new Error('Hooks for ' + hook + ' are not supported.');
	} else {
		mw = cache.middleware[hook];
	}
	cache.middleware[hook] = mw.concat(fns);
}

function attachQualifier(instance, qualifier) {
	/**
	 * Registers `middleware` to be executed _before_ `hook`.
	 * This is a dynamically added method, that may not be present if otherwise configured in {@link options}.qualifiers.
	 * @method pre
	 * @instance
	 * @memberof GrapplingHook
	 * @param {string} hook - hook name, e.g. `'save'`
	 * @param {(...middleware|middleware[])} [middleware] - middleware to register
	 * @returns {GrapplingHook|thenable} the {@link GrapplingHook} instance itself, or a {@link thenable} if no middleware was provided.
	 * @example
	 * instance.pre('save', function(){
	 *   console.log('before saving');
	 * });
	 * @see {@link GrapplingHook#post} for registering middleware functions to `post` hooks.
	 */
	/**
	 * Registers `middleware` to be executed _after_ `hook`.
	 * This is a dynamically added method, that may not be present if otherwise configured in {@link options}.qualifiers.
	 * @method post
	 * @instance
	 * @memberof GrapplingHook
	 * @param {string} hook - hook name, e.g. `'save'`
	 * @param {(...middleware|middleware[])} [middleware] - middleware to register
	 * @returns {GrapplingHook|thenable} the {@link GrapplingHook} instance itself, or a {@link thenable} if no middleware was provided.
	 * @example
	 * instance.post('save', function(){
	 *   console.log('after saving');
	 * });
	 * @see {@link GrapplingHook#pre} for registering middleware functions to `post` hooks.
	 */
	instance[qualifier] = function() {
		var fns = _.toArray(arguments);
		var hookName = fns.shift();
		var output;
		if(fns.length){ //old skool way with callbacks
			output = this;
		}else{
			output = this.__grappling.opts.createThenable(function(resolve){
				fns = [resolve];
			});
		}
		addMiddleware(this, qualifier + ':' + hookName, fns);
		return output;
	};
}

function init(name, opts) {
	if (arguments.length === 1 && _.isObject(name)) {
		opts = name;
		name = undefined;
	}
	var presets;
	if (name) {
		presets = module.exports.get(name);
	}
	this.__grappling = {
		middleware: {},
		opts: _.defaults({}, opts, presets, {
			strict: true,
			qualifiers: {
				pre: 'pre',
				post: 'post'
			},
			createThenable: function(){
				throw new Error('Instance not set up for thenable creation, please set `opts.createThenable`');
			}
		})
	};
	var q = this.__grappling.opts.qualifiers;
	attachQualifier(this, q.pre);
	attachQualifier(this, q.post);
}

/*
 based on code from Isaac Schlueter's blog post: 
 http://blog.izs.me/post/59142742143/designing-apis-for-asynchrony
 */
function dezalgofy(fn, done) {
	var isSync = true;
	fn(safeDone); //eslint-disable-line no-use-before-define
	isSync = false;
	function safeDone() {
		var args = _.toArray(arguments);
		if (isSync) {
			process.nextTick(function() {
				done.apply(null, args);
			});
		} else {
			done.apply(null, args);
		}
	}
}

function iterateAsyncMiddleware(context, middleware, args, done) {
	done = done || /* istanbul ignore next: untestable */ function(err) {
			if (err) {
				throw err;
			}
		};
	var asyncFinished = false;
	var waiting = [];
	var wait = function(callback) {
		waiting.push(callback);
		return function(err) {
			waiting.splice(waiting.indexOf(callback), 1);
			if (asyncFinished !== done) {
				if (err || (asyncFinished && !waiting.length)) {
					done(err);
				}
			}
		};
	};
	async.eachSeries(middleware, function(callback, next) {
		var d = callback.length - args.length;
		switch (d) {
			case 1: //async series
				callback.apply(context, args.concat(next));
				break;
			case 2: //async parallel
				callback.apply(context, args.concat(next, wait(callback)));
				break;
			default :
				//synced
				var err;
				var result;
				try {
					result = callback.apply(context, args);
				} catch (e) {
					err = e;
				}
				if (!err && module.exports.isThenable(result)) {
					//thenable
					result.then(function() {
						next();
					}, next);
				} else {
					//synced
					next(err);
				}
		}
	}, function(err) {
		asyncFinished = (err) ? done : true;
		if (err || !waiting.length) {
			done(err);
		}
	});
}

function iterateSyncMiddleware(context, middleware, args) {
	_.each(middleware, function(callback) {
		callback.apply(context, args);
	});
}

/**
 *
 * @param hookObj
 * @returns {*}
 * @private
 */
function qualifyHook(hookObj) {
	if (!hookObj.name || !hookObj.type) {
		throw new Error('Only qualified hooks are allowed, e.g. "pre:save", not "save"');
	}
	return hookObj;
}

function createHooks(instance, config) {
	var q = instance.__grappling.opts.qualifiers;
	_.each(config, function(fn, hook) {
		var hookObj = parseHook(hook);
		instance[hookObj.name] = function() {
			var args = _.toArray(arguments);
			var done = args.pop();
			if (!_.isFunction(done)) {
				throw new Error('Async methods should receive a callback as a final parameter');
			}
			var results;
			dezalgofy(function(safeDone) {
				async.series([function(next) {
					iterateAsyncMiddleware(instance, instance.getMiddleware(q.pre + ':' + hookObj.name), args, next);
				}, function(next) {
					fn.apply(instance, args.concat(function() {
						var args = _.toArray(arguments);
						var err = args.shift();
						results = args;
						next(err);
					}));
				}, function(next) {
					iterateAsyncMiddleware(instance, instance.getMiddleware(q.post + ':' + hookObj.name), args, next);
				}], function(err) {
					safeDone.apply(null, [err].concat(results));
				});
			}, done);
		};
	});
}

function createSyncHooks(instance, config) {
	var q = instance.__grappling.opts.qualifiers;
	_.each(config, function(fn, hook) {
		var hookObj = parseHook(hook);
		instance[hookObj.name] = function() {
			var args = _.toArray(arguments);
			var middleware = instance.getMiddleware(q.pre + ':' + hookObj.name);
			var result;
			middleware.push(function() {
				result = fn.apply(instance, args);
			});
			middleware = middleware.concat(instance.getMiddleware(q.post + ':' + hookObj.name));
			iterateSyncMiddleware(instance, middleware, args);
			return result;
		};
	});
}

function createThenableHooks(instance, config) {
	var opts = instance.__grappling.opts;
	var q = instance.__grappling.opts.qualifiers;
	_.each(config, function(fn, hook) {
		var hookObj = parseHook(hook);
		instance[hookObj.name] = function() {
			var args = _.toArray(arguments);
			var deferred = {};
			var thenable = opts.createThenable(function(resolve, reject) {
				deferred.resolve = resolve;
				deferred.reject = reject;
			});
			async.series([function(next) {
				iterateAsyncMiddleware(instance, instance.getMiddleware(q.pre + ':' + hookObj.name), args, next);
			}, function(next) {
				fn.apply(instance, args).then(function(result) {
					deferred.result = result;
					next();
				}, next);
			}, function(next) {
				iterateAsyncMiddleware(instance, instance.getMiddleware(q.post + ':' + hookObj.name), args, next);
			}], function(err) {
				if (err) {
					return deferred.reject(err);
				}
				return deferred.resolve(deferred.result);
			});
			
			return thenable;
		};
	});
}

function addHooks(instance, args) {
	var config = {};
	_.each(args, function(mixed) {
		if (_.isString(mixed)) {
			var hookObj = parseHook(mixed);
			var fn = instance[hookObj.name];
			if (!fn) throw new Error('Cannot add hooks to undeclared method:"' + hookObj.name + '"'); //non-existing method
			config[mixed] = fn;
		} else if (_.isObject(mixed)) {
			_.defaults(config, mixed);
		} else {
			throw new Error('`addHooks` expects (arrays of) Strings or Objects');
		}
	}, instance);
	instance.allowHooks(_.keys(config));
	return config;
}

function parseCallHookParams(instance, args) {
	return {
		context: (_.isString(args[0])) ? instance : args.shift(),
		hook: args.shift(),
		args: args
	};
}

/**
 * Grappling hook
 * @alias GrapplingHook
 * @mixin
 */
var methods = {

	/**
	 * Adds middleware to a qualified hook.
	 * Convenience method which allows you to add middleware dynamically more easily.
	 *
	 * @param {String} qualifiedHook - qualified hook e.g. `pre:save`
	 * @param {(...middleware|middleware[])} middleware - middleware to call
	 * @instance
	 * @public
	 * @example
	 * instance.hook('pre:save', function(next) {
	 *   console.log('before saving');
	 *   next();
	 * }
	 * @returns {GrapplingHook|thenable}
	 */
	hook: function() {
		var fns = _.toArray(arguments);
		var hook = fns.shift();
		var output;
		qualifyHook(parseHook(hook));
		if(fns.length){
			output = this;
		}else{
			output = this.__grappling.opts.createThenable(function(resolve){
				fns = [resolve];
			});
		}
		addMiddleware(this, hook, fns);
		return output;
	},

	/**
	 * Removes {@link middleware} for `hook`
	 * @instance
	 * @example
	 * //removes `onPreSave` Function as a `pre:save` middleware
	 * instance.unhook('pre:save', onPreSave);
	 * @example
	 * //removes all middleware for `pre:save`
	 * instance.unhook('pre:save');
	 * @example
	 * //removes all middleware for `pre:save` and `post:save`
	 * instance.unhook('save');
	 * @example
	 * //removes ALL middleware
	 * instance.unhook();
	 * @param {String} [hook] - (qualified) hooks e.g. `pre:save` or `save`
	 * @param {(...middleware|middleware[])} [middleware] - function(s) to be removed
	 * @returns {GrapplingHook}
	 */
	unhook: function() {
		var fns = _.toArray(arguments);
		var hook = fns.shift();
		var hookObj = parseHook(hook);
		var middleware = this.__grappling.middleware;
		var q = this.__grappling.opts.qualifiers;
		if (hookObj.type || fns.length) {
			qualifyHook(hookObj);
			if (middleware[hook]) middleware[hook] = (fns.length ) ? _.without.apply(null, [middleware[hook]].concat(fns)) : [];
		} else if (hookObj.name) {
			/* istanbul ignore else: nothing _should_ happen */
			if (middleware[q.pre + ':' + hookObj.name]) middleware[q.pre + ':' + hookObj.name] = [];
			/* istanbul ignore else: nothing _should_ happen */
			if (middleware[q.post + ':' + hookObj.name]) middleware[q.post + ':' + hookObj.name] = [];
		} else {
			_.each(middleware, function(callbacks, hook) {
				middleware[hook] = [];
			});
		}
		return this;
	},

	/**
	 * Determines whether registration of middleware to `qualifiedHook` is allowed. (Always returns `true` for lenient instances)
	 * @instance
	 * @param {String|String[]} qualifiedHook - qualified hook e.g. `pre:save`
	 * @returns {boolean}
	 */
	hookable: function(qualifiedHook) { //eslint-disable-line no-unused-vars
		if (!this.__grappling.opts.strict) {
			return true;
		}
		var args = _.flatten(_.toArray(arguments));
		return _.every(args, function(qualifiedHook) {
			qualifyHook(parseHook(qualifiedHook));
			return !!this.__grappling.middleware[qualifiedHook];
		}, this);
	},

	/**
	 * Explicitly declare hooks
	 * @instance
	 * @param {(...string|string[])} hooks - (qualified) hooks e.g. `pre:save` or `save`
	 * @returns {GrapplingHook}
	 */
	allowHooks: function() {
		var args = _.flatten(_.toArray(arguments));
		var q = this.__grappling.opts.qualifiers;
		_.each(args, function(hook) {
			if (!_.isString(hook)) {
				throw new Error('`allowHooks` expects (arrays of) Strings');
			}
			var hookObj = parseHook(hook);
			var middleware = this.__grappling.middleware;
			if (hookObj.type) {
				if (hookObj.type !== q.pre && hookObj.type !== q.post) {
					throw new Error('Only "' + q.pre + '" and "' + q.post + '" types are allowed, not "' + hookObj.type + '"');
				}
				middleware[hook] = middleware[hook] || [];
			} else {
				middleware[q.pre + ':' + hookObj.name] = middleware[q.pre + ':' + hookObj.name] || [];
				middleware[q.post + ':' + hookObj.name] = middleware[q.post + ':' + hookObj.name] || [];
			}
		}, this);
		return this;
	},

	/**
	 * Wraps asynchronous methods/functions with `pre` and/or `post` hooks
	 * @instance
	 * @see {@link GrapplingHook#addSyncHooks} for wrapping synchronous methods
	 * @see {@link GrapplingHook#addThenableHooks} for wrapping thenable methods
	 * @example
	 * //wrap existing methods
	 * instance.addHooks('save', 'pre:remove');
	 * @example
	 * //add method and wrap it
	 * instance.addHooks({
	 *   save: instance._upload,
	 *   "pre:remove": function(){
	 *   	//...
	 *   }
	 * });
	 * @param {(...String|String[]|...Object|Object[])} methods - method(s) that need(s) to emit `pre` and `post` events
	 * @returns {GrapplingHook}
	 */
	addHooks: function() {
		var config = addHooks(this, _.flatten(_.toArray(arguments)));
		createHooks(this, config);
		return this;
	},

	/**
	 * Wraps synchronous methods/functions with `pre` and/or `post` hooks
	 * @since 2.4.0
	 * @instance
	 * @see {@link GrapplingHook#addHooks} for wrapping asynchronous methods
	 * @see {@link GrapplingHook#addThenableHooks} for wrapping thenable methods
	 * @param {(...String|String[]|...Object|Object[])} methods - method(s) that need(s) to emit `pre` and `post` events
	 * @returns {GrapplingHook}
	 */
	addSyncHooks: function() {
		var config = addHooks(this, _.flatten(_.toArray(arguments)));
		createSyncHooks(this, config);
		return this;
	},

	/**
	 * Wraps thenable methods/functions with `pre` and/or `post` hooks
	 * @since 3.0.0
	 * @instance
	 * @see {@link GrapplingHook#addHooks} for wrapping asynchronous methods
	 * @see {@link GrapplingHook#addSyncHooks} for wrapping synchronous methods
	 * @param {(...String|String[]|...Object|Object[])} methods - method(s) that need(s) to emit `pre` and `post` events
	 * @returns {GrapplingHook}
	 */
	addThenableHooks: function() {
		var config = addHooks(this, _.flatten(_.toArray(arguments)));
		createThenableHooks(this, config);
		return this;
	},

	/**
	 * Calls all middleware subscribed to the asynchronous `qualifiedHook` and passes remaining parameters to them
	 * @instance
	 * @see {@link GrapplingHook#callSyncHook} for calling synchronous hooks
	 * @see {@link GrapplingHook#callThenableHook} for calling thenable hooks
	 * @param {*} [context] - the context in which the middleware will be called
	 * @param {String} qualifiedHook - qualified hook e.g. `pre:save`
	 * @param {...*} [parameters] - any parameters you wish to pass to the middleware.
	 * @param {Function} [callback] - will be called when all middleware have finished
	 * @returns {GrapplingHook}
	 */
	callHook: function() {
		//todo: decide whether we should enforce passing a callback
		var params = parseCallHookParams(this, _.toArray(arguments));
		params.done = (_.isFunction(params.args[params.args.length - 1])) ? params.args.pop() : null;
		if(params.done){
			var self = this;
			dezalgofy(function(safeDone) {
				iterateAsyncMiddleware(params.context, self.getMiddleware(params.hook), params.args, safeDone);
			}, params.done);
		}else{
			iterateAsyncMiddleware(params.context, this.getMiddleware(params.hook), params.args);
		}
		return this;
	},

	/**
	 * Calls all middleware subscribed to the synchronous `qualifiedHook` and passes remaining parameters to them
	 * @since 2.4.0
	 * @instance
	 * @see {@link GrapplingHook#callHook} for calling asynchronous hooks
	 * @see {@link GrapplingHook#callThenableHook} for calling thenable hooks
	 * @param {*} [context] - the context in which the middleware will be called
	 * @param {String} qualifiedHook - qualified hook e.g. `pre:save`
	 * @param {...*} [parameters] - any parameters you wish to pass to the middleware.
	 * @returns {GrapplingHook}
	 */
	callSyncHook: function() {
		var params = parseCallHookParams(this, _.toArray(arguments));
		iterateSyncMiddleware(params.context, this.getMiddleware(params.hook), params.args);
		return this;
	},

	/**
	 * Calls all middleware subscribed to the synchronous `qualifiedHook` and passes remaining parameters to them
	 * @since 3.0.0
	 * @instance
	 * @see {@link GrapplingHook#callHook} for calling asynchronous hooks
	 * @see {@link GrapplingHook#callSyncHook} for calling synchronous hooks
	 * @param {*} [context] - the context in which the middleware will be called
	 * @param {String} qualifiedHook - qualified hook e.g. `pre:save`
	 * @param {...*} [parameters] - any parameters you wish to pass to the middleware.
	 * @returns {thenable} - a thenable, as created with {@link options}.createThenable
	 */
	callThenableHook: function() {
		var params = parseCallHookParams(this, _.toArray(arguments));
		var deferred = {};
		var thenable = this.__grappling.opts.createThenable(function(resolve, reject) {
			deferred.resolve = resolve;
			deferred.reject = reject;
		});
		var self = this;
		dezalgofy(function(safeDone) {
			iterateAsyncMiddleware(params.context, self.getMiddleware(params.hook), params.args, safeDone);
		}, function(err) {
			if (err) {
				return deferred.reject(err);
			}
			return deferred.resolve();
		});
		return thenable;
	},

	/**
	 * Retrieve all {@link middleware} registered to `qualifiedHook`
	 * @instance
	 * @param qualifiedHook - qualified hook, e.g. `pre:save`
	 * @returns {middleware[]}
	 */
	getMiddleware: function(qualifiedHook) {
		qualifyHook(parseHook(qualifiedHook));
		var middleware = this.__grappling.middleware[qualifiedHook];
		if (middleware) {
			return middleware.slice(0);
		}
		return [];
	},

	/**
	 * Determines whether any {@link middleware} is registered to `qualifiedHook`.
	 * @instance
	 * @param {string} qualifiedHook - qualified hook, e.g. `pre:save`
	 * @returns {boolean}
	 */
	hasMiddleware: function(qualifiedHook) {
		return this.getMiddleware(qualifiedHook).length > 0;
	}
};

/**
 * alias for {@link GrapplingHook#addHooks}.
 * @since 3.0.0
 * @name GrapplingHook#addAsyncHooks
 * @instance
 * @method
 */
methods.addAsyncHooks = methods.addHooks;
/**
 * alias for {@link GrapplingHook#callHook}.
 * @since 3.0.0
 * @name GrapplingHook#callAsyncHook
 * @instance
 * @method
 */
methods.callAsyncHook = methods.callHook;

/**
 * @module grappling-hook
 * @type {exports|module.exports}
 */
module.exports = {
	/**
	 * Mixes {@link GrapplingHook} methods into `instance`.
	 * @see {@link module:grappling-hook.attach attach} for attaching {@link GrapplingHook} methods to prototypes.
	 * @see {@link module:grappling-hook.create create} for creating {@link GrapplingHook} instances.
	 * @param {Object} instance
	 * @param {string} [presets] - presets name, see {@link module:grappling-hook.set set}
	 * @param {options} [opts] - {@link options}.
	 * @mixes GrapplingHook
	 * @returns {GrapplingHook}
	 * @example
	 * var grappling = require('grappling-hook');
	 * var instance = {
	 * };
	 * grappling.mixin(instance); // add grappling-hook functionality to an existing object
	 */
	mixin: function mixin(instance, presets, opts) {//eslint-disable-line no-unused-vars
		var args = _.toArray(arguments);
		instance = args.shift();
		init.apply(instance, args);
		_.extend(instance, methods);
		return instance;
	},

	/**
	 * Creates an object with {@link GrapplingHook} functionality.
	 * @see {@link module:grappling-hook.attach attach} for attaching {@link GrapplingHook} methods to prototypes.
	 * @see {@link module:grappling-hook.mixin mixin} for mixing {@link GrapplingHook} methods into instances.
	 * @param {string} [presets] - presets name, see {@link module:grappling-hook.set set}
	 * @param {options} [opts] - {@link options}.
	 * @returns {GrapplingHook}
	 * @example
	 * var grappling = require('grappling-hook');
	 * var instance = grappling.create(); // create an instance
	 */
	create: function create(presets, opts) {//eslint-disable-line no-unused-vars
		return module.exports.mixin.apply(null, [{}].concat(_.toArray(arguments)));
	},

	/**
	 * Attaches {@link GrapplingHook} methods to `base`'s `prototype`.
	 * @see {@link module:grappling-hook.create create} for creating {@link GrapplingHook} instances.
	 * @see {@link module:grappling-hook.mixin mixin} for mixing {@link GrapplingHook} methods into instances.
	 * @param {Function} base
	 * @param {string} [presets] - presets name, see {@link module:grappling-hook.set set}
	 * @param {options} [opts] - {@link options}.
	 * @mixes GrapplingHook
	 * @returns {Function}
	 * @example
	 * var grappling = require('grappling-hook');
	 * var MyClass = function() {
	 * };
	 * MyClass.prototype.save = function(done) {
	 *   console.log('save!');
	 *   done();
	 * };
	 * grappling.attach(MyClass); // attach grappling-hook functionality to a 'class'
	 */
	attach: function attach(base, presets, opts) {//eslint-disable-line no-unused-vars
		var args = _.toArray(arguments);
		args.shift();
		var proto = (base.prototype) ? base.prototype : base;
		_.each(methods, function(fn, methodName) {
			proto[methodName] = function() {
				init.apply(this, args);
				_.each(methods, function(fn, methodName) {
					this[methodName] = fn.bind(this);
				}, this);
				return fn.apply(this, _.toArray(arguments));
			};
		});
		return base;
	},

	/**
	 * Store `presets` as `name`. Or set a specific value of a preset.
	 * (The use of namespaces is to avoid the very unlikely case of name conflicts with deduped node_modules)
	 * @since 3.0.0
	 * @see {@link module:grappling-hook.get get} for retrieving presets
	 * @param {string} name
	 * @param {options} options
	 * @returns {module:grappling-hook}
	 * @example
	 * //index.js - declaration
	 * var grappling = require('grappling-hook');
	 * grappling.set('grapplinghook:example', {
	 *   strict: false,
	 *   qualifiers: {
	 *     pre: 'before',
	 *     post: 'after'
	 *   }
	 * });
	 *
	 * //foo.js - usage
	 * var instance = grappling.create('grapplinghook:example'); // uses options as cached for 'grapplinghook:example'
	 * @example
	 * grappling.set('grapplinghook:example.qualifiers.pre', 'first');
	 * grappling.set('grapplinghook:example.qualifiers.post', 'last');
	 */
	set: function(name, options) {
		_.set(presets, name, options);
		return module.exports;
	},

	/**
	 * Retrieves presets stored as `name`. Or a specific value of a preset.
	 * (The use of namespaces is to avoid the very unlikely case of name conflicts with deduped node_modules)
	 * @since 3.0.0
	 * @see {@link module:grappling-hook.set set} for storing presets
	 * @param {string} name
	 * @returns {*}
	 * @example
	 * grappling.get('grapplinghook:example.qualifiers.pre');
	 * @example
	 * grappling.get('grapplinghook:example.qualifiers');
	 * @example
	 * grappling.get('grapplinghook:example');
	 */
	get: function(name) {
		return _.get(presets, name);
	},

	/**
	 * Determines whether `subject` is a {@link thenable}.
	 * @param {*} subject
	 * @returns {Boolean}
	 * @see {@link thenable}
	 */
	isThenable: function isThenable(subject) {
		return subject && subject.then && _.isFunction(subject.then);
	}
};