router.js

'use strict';

const debug = require('debug')('ffz-api-router');
const FindMyWay = require('find-my-way');
const pathToRegexp = require('path-to-regexp');
const METHODS = require('methods');
const formatURL = require('url').format;
const reuse = require('reusify');

const compose = require('./composer');
const Mount = require('./mount');

const has = (thing, key) => Object.prototype.hasOwnProperty.call(thing, key);
const NOOP = () => {};

const HOST_OPTS = {
	delimiter: '.',
	strict: true,
	sensitive: false
};


module.exports = Router;

/**
 * Create a new Router.
 *
 * @constructor
 * @param {Object} [opts] Options for initializing this Router and the internal find-my-way router.
 * @param {String} [opts.host] An optional host for this router. Used when generating URLs. Can include variables, though that's slower and not recommended.
 * @param {String} [opts.name] An optional name for this router. Used when resolving a route for URL generation.
 * @param {String} [opts.prefix] An optional prefix to apply to all routes for this Router.
 * @param {Object} [opts.findMyWay] Options to provide to the internal find-my-way router.
 * @param {Boolean} [opts.paramwareBeforeDataware=false] If this is true, middleware registered for specific params will run before data-based middleware.
 * @param {Boolean|Router~dataMiddleware} [opts.mountMiddleware] Middleware to use for {@link Router#mount} instead of {@link Mount}. If set to false, this will disable the automatic `mount` data-based middleware. As a result, {@link Router#mount} will not function correctly.
 * @param {Boolean|Router~handleOptions} [opts.handleOptions] Whether or not to handle OPTIONS requests. Optionally takes a function to handle OPTIONS.
 * @param {Boolean|Router~handleOptions} [opts.handle405] Whether or not to generate 405 responses when there are no matching methods for a route. Optionally takes a function to handle 405s.
 */
function Router(opts) {
	if ( !(this instanceof Router) )
		return new Router(opts);

	this.opts = opts || {};
	this.opts.paramwareBeforeDataware = this.opts.paramwareBeforeDataware || false;
	if ( this.opts.mountMiddleware === undefined )
		this.opts.mountMiddleware = Mount;

	this.opts.handle405 = this.opts.handle405 == null ? true : this.opts.handle405;
	this.opts.handleOptions = this.opts.handleOptions == null ? true : this.opts.handleOptions;

	// Routers that we interact with.
	this._parents = [];
	this._nested = {};

	// Content defined on *this* Router.
	this.routes = [];
	this.middlewares = [];

	this.paramwares = {};
	this.datawares = {};

	this.dataware_sort = {};
	this.dataware_default = {};
	this.dataware_exclusive = new Set;

	if ( this.opts.mountMiddleware ) {
		if ( typeof this.opts.mountMiddleware !== 'function' )
			throw new TypeError('opts.mountMiddleware must be a function');

		this.datawares.mount = [this.opts.mountMiddleware];
		this.dataware_sort.mount = -10;
		this.dataware_exclusive.add('mount');
	}

	// Internals
	this._routers = [];
	this._routes = new Map;
	this._named = {};
	this._middlewares = {};

	this._live = false;
}

/**
 * A Koa middleware function that has an additional parameter containing the internal
 * route data, called when a route is matched but the requested method has no handler.
 *
 * These functions are used for generating `405 Method Not Allowed` responses, as well
 * as responses to `OPTIONS` requests.
 *
 * @example
 * const router = Router({
 *     handleOptions: (ctx, next, store) => {
 *         ctx.set('Allow', Object.keys(store).join(', ').toUpperCase());
 *         ctx.status = 204;
 *     }
 * })
 *
 * @callback Router~handleOptions
 * @param {Object} ctx The Koa Context
 * @param {Function} next The next middleware in the stack
 * @param {Object} store The internal route object, with keys for each handled method
 */

/**
 * A factory for Koa middleware, acting off of data attached to a specific route.
 * The returned function or functions may have a `sort` property for overriding the
 * sorting of that specific middleware instance on that specific route.
 *
 * @example
 * router.useData('database', options => {
 *     const connection = database.getConnection(options);
 *
 *     return async (ctx, next) => {
 *         const cursor = connection.getCursor();
 *         ctx.db = cursor;
 *         try {
 *             await next();
 *         } finally {
 *             cursor.close();
 *             ctx.db = null;
 *         }
 *     }
 * });
 *
 * router.useData('validation', options => {
 *     const methods = [];
 *
 *     if ( options.query )
 *         methods.push((ctx, next) => {
 *             validateQuery(options.query, ctx.query);
 *             return next();
 *         });
 *
 *     if ( options.result ) {
 *         const fn = async (ctx, next) => {
 *             await next();
 *             validateResult(options.result, ctx);
 *         };
 *
 *         fn.sort = 2;
 *         methods.push(fn);
 *     }
 *
 *     return methods;
 * });
 *
 * @callback Router~dataMiddleware
 * @param {*} data The data attached to a route that triggered the instantiation of this
 * data-ware middleware.
 * @param {String} route The applicable route
 * @param {Object} _store The internal data store for the route.
 * @returns {Function|Function[]} Koa Middleware
 */

// Middleware

/**
 * Get a middleware method for adding a {@link Router} to Koa.
 *
 * @example
 * const router = Router();
 * app.use(router.middleware())
 *
 * @returns {Function} Koa Middleware
 */
Router.prototype.middleware = function() {
	const router = this;

	if ( ! this._live ) {
		this._live = true;
		this._update();
	}

	// Context URL Generation
	let pool;

	function URLFor() {
		this.context = null;

		const that = this;

		this.urlFor = function(name, params, options) {
			if ( name.startsWith('.') ) {
				const name_prefix = that.context.state._route.name_prefix;
				if ( name_prefix )
					name = name_prefix + name;
				else
					name = name.slice(1);
			}

			return router.urlFor(name, params, options, that.context.request.host, that.context.request.protocol);
		}

		this.done = function(stuff) {
			that.context.urlFor = null;
			that.context = null;
			pool.release(that);
			return stuff;
		}

		this.catch = function(err) {
			that.context.urlFor = null;
			that.context = null;
			pool.release(that);
			throw err;
		}
	}

	pool = reuse(URLFor);

	// The Middleware
	const dispatch = function dispatch(ctx, next) {
		debug('%s %s', ctx.method, ctx.path);
		const has_timers = ctx.startTimer;

		if ( has_timers )
			ctx.startTimer('routing');

		const method = ctx.method.toLowerCase();
		let match, host_params;

		if ( router._routers ) {
			const host = ctx.request.host ? ctx.request.host.toLowerCase() : '';
			for(const [fn, matcher, rt, plucker] of router._routers) {
				let fn_match;
				if ( fn === -1 || (fn === 0 && matcher === host) || (fn === 1 && (plucker ? (fn_match = matcher.exec(host)) : matcher.test(host))) ) {
					match = rt.find('GET', ctx.path);
					if ( match ) {
						host_params = plucker && fn_match ? plucker(fn_match) : null;
						break;
					}
				}
			}
		}

		if ( ! match ) {
			if ( has_timers )
				ctx.stopTimer('routing');
			return next();
		}

		const {params, store} = match;
		const data = store[method] || (method !== 'options' && store.all);

		ctx.request.host_params = ctx.host_params = host_params;
		ctx.request.params = ctx.params = params;
		ctx.state._route = data;
		ctx.state.route = data && data.options;

		const urls = pool.get();
		urls.context = ctx;
		ctx.urlFor = urls.urlFor;

		if ( data ) {
			if ( has_timers )
				ctx.stopTimer('routing');
			return Promise.resolve(data.fn(ctx, next)).then(urls.done).catch(urls.catch);
		}

		let fn;
		if ( method === 'options' && router.opts.handleOptions ) {
			if ( typeof router.opts.handleOptions === 'function' )
				fn = router.opts.handleOptions;
			else
				fn = handleOptions;

		} else if ( ! router.opts.handle405 ) {
			if ( has_timers )
				ctx.stopTimer('routing');

			return Promise.resolve(next()).then(urls.done).catch(urls.catch);

		} else if ( typeof router.opts.handle405 === 'function' )
			fn = router.opts.handle405;
		else
			fn = handle405;

		if ( has_timers )
			ctx.stopTimer('routing');

		return Promise.resolve(fn(ctx, next, store)).then(urls.done).catch(urls.catch)
	}

	dispatch.router = this;
	return dispatch;
}


function handle405(ctx, next, store) {
	const allowed = store.all ? METHODS : Object.keys(store);
	ctx.throw(405, undefined, {
		headers: {
			Allow: allowed.join(', ').toUpperCase()
		}
	});
}


function handleOptions(ctx, next, store) {
	const allowed = store.all ? METHODS : Object.keys(store);
	ctx.set('Allow', allowed.join(', ').toUpperCase());
	ctx.status = 204;
}


// Events

/**
 * Call the {@link Router#_update} method of each {@link Router} that
 * has this Router nested.
 * @private
 */
Router.prototype._updateParents = function() {
	for(const parent of this._parents)
		parent._update();
}


/**
 * Re-build the internal route and middleware cache with updated data
 * and then call {@link Router#_updateParents}.
 * @private
 */
Router.prototype._update = function() {
	const live = this._live;

	if ( live )
		this._routers = [];
		/*if ( this._router )
			this._router.reset();
		else
			this._router = FindMyWay(this.opts.findMyWay);*/

	const prefix = this.opts.prefix || '',
		name = this.opts.name,
		host = this.opts.host,
		hosts = new Map,
		routes = this._routes = new Map,
		named = this._named = {},
		middlewares = this._middlewares = {};

	// Route and Middleware Merging
	mergeRoutes(routes, host, name, prefix, this.routes);
	mergeMiddlewares(middlewares, prefix, this.middlewares);

	for(const [nest_path, nested] of Object.entries(this._nested))
		for(const router of nested) {
			const pref = `${prefix}${nest_path || ''}`;

			mergeRoutes(routes, host, name, pref, router._routes);
			mergeMiddlewares(middlewares, pref, router._middlewares);
		}


	// Convert all middleware paths to special regex so that we can easilly
	// determine which middleware should be associated with which routes.
	const middleware_tokens = Object.entries(middlewares).map(([route, data]) =>
		[pathToRegexp.parse(route), route, data]
	);

	const dataware = Object.entries(this.datawares);

	// Now, we want to build our final routing data. This data is kept
	// on our find-my-way router instance. We're also going to apply
	// any dataware at this stage.
	for(const [host, hosted_routes] of routes.entries()) {
		for(const [route, data] of Object.entries(hosted_routes)) {
			const route_tokens = pathToRegexp.parse(route),
				route_fn = pathToRegexp.tokensToFunction(route_tokens),
				route_params = route_tokens
					.filter(token => typeof token !== 'string')
					.map(token => token.name),

				matching_pware = data._paramware = data._paramware || [],
				md = this._matchMiddleware(route, middleware_tokens),
				new_data = {};

			let route_host;

			for(const name of route_params) {
				const pware = this.paramwares[name];
				if ( pware )
					for(const pw of pware)
						matching_pware.push(pw);
			}

			// We need to know the host ahead of time because of URL generation,
			// so check for an override right now.
			for(const d of Object.values(data)) {
				if ( d.options && d.options.host ) {
					if ( route_host != null )
						throw new Error(`Route has conflicting hosts: ${route}`);

					route_host = d.options.host;
				}
			}

			if ( route_host == null )
				route_host = host;

			let host_info = hosts.get(route_host);
			if ( ! host_info ) {
				hosts.set(route_host, host_info = {});
				if ( route_host ) {
					host_info.tokens = pathToRegexp.parse(route_host, HOST_OPTS);
					host_info.rich = host_info.tokens.length > 1 || typeof host_info.tokens[0] != 'string';
					if ( host_info.rich ) {
						host_info.vars = [];
						host_info.reverse = pathToRegexp.compile(route_host, HOST_OPTS);
						host_info.matcher = pathToRegexp.tokensToRegExp(host_info.tokens, host_info.vars, HOST_OPTS);

						for(let i=0, l = host_info.vars.length; i < l; i++)
							host_info.vars[i] = host_info.vars[i] && host_info.vars[i].name;
					}
				}
			}

			for(const [key, d] of Object.entries(data)) {
				if ( key.startsWith('_') )
					continue;

				if ( d.options && d.options.name ) {
					const name = d.name_prefix ?
						`${d.name_prefix}.${d.options.name}` :
						d.options.name;

					named[name] = [route_params, route_fn, host_info.rich, route_host, host_info.vars, host_info.reverse];
				}

				const matching_defaults = d.defaults = d.defaults || {};
				for(const [dkey, value] of Object.entries(this.dataware_default)) {
					if ( ! has(matching_defaults, dkey) )
						matching_defaults[dkey] = value;
				}

				const matching_dware = d.dataware = d.dataware || [];
				for(const [dkey, dware] of dataware) {
					if ( ! d.exclusive.includes(dkey) && (has(d.options, dkey) || has(matching_defaults, dkey)) )
						for(const dw of dware)
							matching_dware.push([dkey, dw, this.dataware_sort[dkey]]);
				}

				for(const dkey of this.dataware_exclusive)
					d.exclusive.push(dkey);

				if ( live ) {
					// Construct our list of dataware methods.
					// Dataware constructors are allowed to return more
					// than one function to be composed, and they
					// can have custom sorting.
					const dware = [];
					for(const [dkey, dw, sort] of matching_dware) {
						const data = has(d.options, dkey) ? d.options[dkey] : matching_defaults[dkey],
							out = dw(data, route, d);
						if ( ! out )
							continue;

						if ( Array.isArray(out) ) {
							for(const thing of out) {
								if ( ! thing )
									continue;

								if ( ! thing.sort )
									thing.sort = sort;

								dware.push(thing);
							}

						} else {
							if ( ! out.sort )
								out.sort = sort;

							dware.push(out);
						}
					}

					dware.sort((a, b) => {
						a = a && a.sort || 0;
						b = b && b.sort || 0;

						return a - b;
					});

					new_data[key] = Object.assign({}, d, {
						fn: this.opts.paramwareBeforeDataware ?
							compose(md, matching_pware, dware, d.middleware) :
							compose(md, dware, matching_pware, d.middleware)
					})
				}
			}

			if ( live ) {
				if ( ! host_info.router ) {
					const router = host_info.router = FindMyWay(this.opts.FindMyWay);

					if ( host_info.tokens ) {
						if ( host_info.rich ) {
							this._routers.push([1, host_info.matcher, router, pluckVars(host_info.vars)]);
						} else
							this._routers.push([0, route_host.toLowerCase(), router]);

					} else
						this._routers.push([-1, null, router]);
				}

				host_info.router.on('GET', route, NOOP, new_data);
			}
		}
	}

	// Now, let all this trickle down to our parents.
	this._updateParents();
}

/**
 * Pre-calculate which middleware could potentially run on a specific
 * route so that we can minimize the middleware that actually run on
 * any given route.
 *
 * @private
 * @param {String} path The path for the route we're checking
 * @param {Array} middlewares An array of middleware descriptions, including
 * tokens, the raw route, and the middleware functions themselves
 * @returns {Array} The matching middleware to be applied to the route.
 */
Router.prototype._matchMiddleware = function(path, middlewares) {
	const out = [],
		// This works slightly differently than find-my-way's
		// route parsing, but hopefully it's close enough to
		// make middleware matching work.
		tokens = pathToRegexp.parse(path),
		match_fn = this.opts.middlewareMatcher || couldMatch;

	for(const [middle_tokens, r, data] of middlewares) {
		if ( match_fn(tokens, middle_tokens) ) {
			const filtered = r && r.length > 0,
				rich = filtered && r.includes(':'),
				compiled = rich ? pathToRegexp(r) : r;

			for(const middleware of data)
				out.push({
					filtered,
					rich,
					test: compiled,
					fn: middleware
				});
		}
	}

	return out;
}


function pluckVars(vars) {
	if ( ! Array.isArray(vars) || ! vars.length )
		return null;

	const len = vars.length,
		keys = vars.map(v => v.name || v);

	return match => {
		const plucked = {};
		if ( match && match.length ) {
			for(let i=0; i < len; i++)
				plucked[keys[i]] = match[i + 1];
		}

		return plucked;
	}
}


function couldMatch(tokens, middle_tokens) {
	// TODO: Make this way smarter.
	// Right now, we're just comparing the first tokens. And then,
	// we're only comparing them if they're both strings.
	// If nothing else, we should try to count segments.

	//const len = tokens.length,
	//	mid_len = middle_tokens.length;

	const i=0, j=0;

	//while(i < len || j < mid_len) {
	const token = tokens[i],
		mid_token = middle_tokens[j];

	if ( ! token )
		return false;

	if ( typeof token === 'string' && typeof mid_token === 'string' ) {
		if ( token !== mid_token && ! token.startsWith(`${mid_token}/`) )
			return false;
	}

	/*i++;
		j++;*/
	//}

	return true;
}


function mergeRoutes(output, host, name, prefix, routes) {
	if ( Array.isArray(routes) )
		routes = [[host, routes]];
	else if ( routes instanceof Map )
		routes = Array.from(routes.entries());
	else
		routes = Object.entries(routes);

	const mapped = output instanceof Map;

	for(const [route_host, host_data] of routes) {
		const use_host = route_host || host;
		let hosted;
		if ( mapped ) {
			hosted = output.get(use_host);
			if ( ! hosted )
				output.set(use_host, hosted = {});
		} else
			hosted = output[use_host] = output[use_host] || {};

		const host_routes = Array.isArray(host_data) ? host_data : Object.entries(host_data);

		for(const data of host_routes) {
			let paths = data[0];
			if ( ! Array.isArray(paths) )
				paths = [paths];

			for(const p of paths) {
				const prefixed = `${prefix}${p}`,
					out = hosted[prefixed] = hosted[prefixed] || {};

				for(const [method, rdata] of Object.entries(data[1])) {
					if ( method.startsWith('_') ) {
						if ( Array.isArray(rdata) )
							out[method] = Array.from(rdata);
						else if ( typeof rdata === 'object' )
							out[method] = Object.assign({}, rdata);
						else
							out[method] = rdata;

						continue;
					}

					let name_prefix = rdata.name_prefix;
					if ( name )
						name_prefix = name_prefix ? `${name}.${name_prefix}` : name;

					out[method] = Object.assign(
						{},
						rdata,
						{
							name_prefix,
							dataware: rdata.dataware ? Array.from(rdata.dataware) : [],
							exclusive: rdata.exclusive ? Array.from(rdata.exclusive) : [],
							defaults: rdata.defaults ? Object.assign({}, rdata.defaults) : {}
						}
					);
				}
			}
		}
	}

	return output;
}


function mergeMiddlewares(output, prefix, middlewares) {
	if ( ! Array.isArray(middlewares) )
		middlewares = Object.entries(middlewares);

	for(const data of middlewares) {
		let paths = data[0];
		if ( ! Array.isArray(paths) )
			paths = [paths];

		for(const p of paths) {
			const prefixed = `${prefix}${p}`,
				out = output[prefixed] = output[prefixed] || [];

			for(const middleware of Array.isArray(data[1]) ? data[1] : [data[1]])
				out.push(middleware);
		}
	}

	return output;
}


// Registering Routes

/*
 * Valid Method Signatures
 *
 * this.get('/blah/:id', ctx => { })
 * this.get(['/blah:id'], ctx => { })
 * this.get('/blah/:id', (ctx, next) => { }, ctx => { })
 * this.get(['/blah:id'], (ctx, next) => { }, ctx => { })
 * this.get('name', '/blah/:id', ctx => { })
 * this.get('name', ['/blah:id'], ctx => { })
 * this.get('name', '/blah/:id', (ctx, next) => { }, ctx => { })
 * this.get('name', ['/blah:id'], (ctx, next) => { }, ctx => { })
 *
 * this.get('/blah/:id', {opts: true}, ctx => { })
 * this.get(['/blah:id'], {opts: true}, ctx => { })
 * this.get('/blah/:id', {opts: true}, (ctx, next) => { }, ctx => { })
 * this.get(['/blah:id'], {opts: true}, (ctx, next) => { }, ctx => { })
 * this.get('name', '/blah/:id', {opts: true}, ctx => { })
 * this.get('name', ['/blah:id'], {opts: true}, ctx => { })
 * this.get('name', '/blah/:id', {opts: true}, (ctx, next) => { }, ctx => { })
 * this.get('name', ['/blah:id'], {opts: true}, (ctx, next) => { }, ctx => { })
 */

METHODS.concat('all').forEach(method => {
	/**
	* Match URL paths to middleware using `router.METHOD()` where `method` is an HTTP method,
	* such as GET, POST, or DELETE. The special method `router.all()` will match all methods.
	*
	* When a route is matched, the route's options will be available at `ctx.state.route`.
	*
	* Route paths are passed directly to an internal [find-my-way](https://www.npmjs.com/package/find-my-way)
	* instance and should be written using that syntax. This syntax, for the most part,
	* mirrors that used by [path-to-regexp](https://github.com/pillarjs/path-to-regexp).
	*
	* If supplied, hosts are parsed with `path-to-regexp`. Hosts without variables are checked
	* with simple string comparison while hosts with variables are matched with a regular
	* expression generated by `path-to-regexp`.
	*
	* Any variables within the host will be stored in `ctx.host_params` and `ctx.request.host_params`.
	*
	* @example
	* router
	*     .get('/', (ctx, next) => {
	*         ctx.body = "Hello, World!"
	*     })
	*     .post('user', '/user/:userID', (ctx, next) => {
	*         // ...
	*     })
	*     .del('/topic/:topicID/message/:messageID', {some_data: false}, (ctx, next) => {
	*         // ...
	*     })
	*
	* @alias METHOD
	* @memberof Router.prototype
	* @param {String} [name] A name for this route. Equivilent to setting a name key in options.
	* @param {String|String[]} path A path or multiple paths that these middleware functions will match.
	* @param {Object} [options] Optional data to associate with the route, including a name and data for data-based middleware.
	* @param {String} [options.host] Optional host for this specific route. Different methods on the same route must use the same host.
	* @param {...Function} middleware Middleware functions to handle this route.
	* @returns {Router} The router
	*/
	Router.prototype[method] = function(name, path, options, ...middleware) {
		// If the first parameter is a string, whether or not it's a name
		// depends on whether or not the second parameter is a path, which
		// can be a string or an array of strings.

		// It's more efficient to check the type of the second parameter directly.
		if ( typeof path !== 'string' && ! Array.isArray(path) ) {
			if ( options != null )
				middleware.unshift(options);

			options = path;
			path = name;
			name = null;
		}

		// If options is a function, it's middleware and not actual options.
		if ( typeof options === 'function' ) {
			middleware.unshift(options);
			options = {};

		} else if ( typeof options !== 'object' || Array.isArray(options) )
			throw new TypeError('options must be an object');

		for(const fn of middleware)
			if ( typeof fn !== 'function' )
				throw new TypeError('middleware must be functions')

		options.name = name;

		this.register([method], path, options, ...middleware);
		return this;
	}
})

// Alias of Router.delete because `delete` is a reserved word.
Router.prototype.del = Router.prototype['delete'];


/**
 * Register a new route and update the router's internal state.
 *
 * @example
 * router.register('get', '/', null, ctx => {
 *     ctx.body = "Hello, World!"
 * })
 *
 * @param {String|String[]} methods The HTTP methods that these middleware functions can handle.
 * @param {String|String[]} path A path or multiple paths that these middleware functions will match.
 * @param {Object|null} options Optional data to associate with the route, including a name and data for data-based middleware.
 * @param {String} [options.host] Optional host for this specific route. Different methods on the same route must use the same host.
 * @param {...Function} middleware Middleware functions to handle this route.
 * @returns {Router} The Router
 */
Router.prototype.register = function(methods, path, options, ...middleware) {
	if ( ! Array.isArray(methods) )
		methods = [methods];

	for(const method of methods)
		if ( typeof method !== 'string' )
			throw new TypeError('method must be a string');

	if ( ! Array.isArray(path) )
		path = [path];

	for(const p of path)
		if ( typeof p !== 'string' )
			throw new TypeError('path must be a string or array of strings');

	if ( options == null )
		options = {};
	else if ( typeof options !== 'object' || Array.isArray(options) )
		throw new TypeError('options must be an object');

	for(const fn of middleware)
		if ( typeof fn !== 'function' )
			throw new TypeError('middleware must be a function or array of functions');

	const route = {};
	for(const method of methods)
		route[method.toLowerCase()] = {options, middleware};

	this.routes.push([path, route]);
	this._update();
	return this;
}


// Registering Middleware

/**
 * Use the given middleware. Middleware are run in the order they are defined.
 * This can also be used to nest another {@link Router} as a child of this
 * router.
 *
 * @example
 * router.use(SomeMiddleware);
 * router.use('/user', SomeUserMiddleware);
 * router.use(anotherRouter);
 *
 * @param {String|String[]} [path] A path or array of paths to limit the middleware to
 * @param {...(Function|Router)} middleware The middleware function(s) to use
 * @returns {Router} The Router
 */
Router.prototype.use = function(path, ...middleware) {
	if ( typeof path === 'function' || path instanceof Router ) {
		middleware.unshift(path);
		path = '';
	}

	if ( ! Array.isArray(path) )
		path = [path];

	for(const p of path)
		if ( typeof p !== 'string' )
			throw new TypeError('path must be a string or array of strings');

	for(const fn of middleware)
		if ( !(fn instanceof Router) && typeof fn !== 'function' )
			throw new TypeError('middleware must be a function or Router instance');

	const mids = [];

	for(let fn of middleware) {
		if ( typeof fn === 'function' && fn.router instanceof Router )
			fn = fn.router;

		if ( fn instanceof Router ) {
			this._nest(path, fn);
		} else
			mids.push(fn);
	}

	if ( mids.length )
		this.middlewares.push([path, mids]);

	this._update();
	return this;
}

/**
 * Use constructed middleware on routes with the provided data key. Constructors
 * registered using this method are executed when pre-calculating a route's middleware
 * chain. The constructors are expected to return a middleware function or array of
 * functions. These functions will be run after general middleware registered
 * via {@link Router#use} but before the middleware functions registered for a route.
 *
 * By setting a `sort` property on the returned middleware method, it is possible
 * to override the sorting for that specific method.
 *
 * @example
 * router.useData('headers', headers => {
 *     return async (ctx, next) => {
 *         await next();
 *         ctx.set(headers);
 *     }
 * });
 *
 * router.useData('validation', -1, options => {
 *     const postFn = (ctx, next) => {
 *         // This runs after headers
 *         await next();
 *     };
 *
 *     // Make postFn run later
 *     postFn.sort = 2;
 *
 *     return [
 *         async (ctx, next) => {
 *             // This runs before headers
 *             await next();
 *         },
 *         postFn
 *     ];
 * });
 *
 * router.get('/', {
 *     headers: {
 *         'Access-Control-Allow-Origin': '*'
 *     },
 *     validation: true
 * }, ctx => {
 *     ctx.body = "Hello, World!";
 * });
 *
 * @param {String} key The data key to match
 * @param {Number} [sort_value=0] A number to use for this data-based middleware when
 * sorting to determine which to apply first. Lower values execute first.
 * @param {...Function} constructor The middleware constructor function(s) to use
 * @returns {Router} The Router
 */
Router.prototype.useData = function(key, sort_value, ...constructor) {
	if ( typeof key !== 'string' )
		throw new TypeError('key must be a string');

	if ( typeof sort_value === 'function' ) {
		constructor.unshift(sort_value);
		sort_value = 0;
	} else if ( typeof sort_value === 'number' )
		this.dataware_sort[key] = sort_value;
	else
		throw new TypeError('sort_value must be a number');

	for(const fn of constructor)
		if ( typeof fn !== 'function' )
			throw new TypeError('middleware must be a function');

	const dws = this.datawares[key] = this.datawares[key] || [];
	for(const fn of constructor)
		dws.push(fn);

	this._update();
	return this;
}

/**
 * This allows you to set default data which is used for all routes that do not
 * have existing data set for a specific data-based middleware.
 *
 * @example
 * // For an example of how to write a simple cache dataware,
 * // check out the project README.
 * router.useData('cache', duration => {
 *     // duration is the number of seconds
 *     // <cache logic goes here>
 *     return cache_middleware;
 * });
 *
 * // We default to a 60 second cache.
 * router.defaultData('cache', 60);
 *
 * router.get('/', ctx => {
 *     ctx.body = 'This uses the default cache of 60 seconds: ' + Date.now();
 * });
 *
 * router.get('/fast', {cache: 5}, ctx => {
 *     ctx.body = 'This is only cached for 5 seconds: ' + Date.now();
 * });
 *
 * @param {String} key The data key
 * @param {Object} value The default value to set. `undefined` to remove.
 * @returns {Router} The Router
 */
Router.prototype.defaultData = function(key, value) {
	if ( typeof key !== 'string' )
		throw new TypeError('key must be a string');

	if ( value === undefined )
		delete this.dataware_default[key];
	else
		this.dataware_default[key] = value;

	this._update();
	return this;
}

/**
 * This allows you to override the order in which data-based middleware are
 * applied. You can also provide this value when defining your data-based
 * middleware constructors with {@link Router#useData}.
 *
 * @param {String} key The data key
 * @param {Number} [sort_value=0] A number to use for this data-based
 * middleware when sorting to determine which to apply first. Lower values
 * execute first.
 * @returns {Router} The Router
 */
Router.prototype.sortData = function(key, sort_value = 0) {
	if ( typeof key !== 'string' )
		throw new TypeError('key must be a string');

	if ( typeof sort_value !== 'number' )
		throw new TypeError('sort_value must be a number');

	this.dataware_sort[key] = sort_value;
	this._update();
	return this;
}

/**
 * This method allows you to mark a specific key for data-based middleware as
 * exclusive. This will prevent a parent's data-based middleware from being
 * applied to the routes of a nested {@link Router}.
 *
 * The default `mount` middleware is set to exclusive to prevent multiple copies
 * of the mount middleware being applied to matching routes.
 * @param {String} key The data key
 * @param {Boolean} [exclusive=true] Whether or not data-based middleware for
 * the provided key should be exclusive.
 * @returns {Router} The Router
 */
Router.prototype.setDataExclusive = function(key, exclusive = true) {
	if ( typeof key !== 'string' )
		throw new TypeError('key must be a string');

	if ( typeof exclusive !== 'boolean' )
		throw new TypeError('exclusive must be a bool');

	if ( exclusive )
		this.dataware_exclusive.add(key);
	else
		this.dataware_exclusive.delete(key);

	this._update();
	return this;
}

/**
 * Use middleware for a named route parameter. This is useful for
 * automatically loading data or performing validation for commonly used
 * route parameters.
 *
 * @example
 * router.param('userID', async (userID, ctx, next) => {
 *     ctx.user = await Users.query().where('id', userID);
 *     return next();
 * });
 *
 * router.get('/user/:userID', ctx => {
 *     // ... do something with ctx.user
 * });
 *
 * @param {String} param The name of the parameter to handle.
 * @param {...Function} middleware The middleware to apply to that parameter.
 */
Router.prototype.param = function(param, ...middleware) {
	if ( typeof param !== 'string' )
		throw new TypeError('param must be a string');

	for(const fn of middleware)
		if ( typeof fn !== 'function' )
			throw new TypeError('middleware must be a function');

	const pws = this.paramwares[param] = this.paramwares[param] || [];
	for(const fn of middleware)
		pws.push((ctx, next) => fn(ctx.params[param], ctx, next));

	this._update();
}

/**
 * Mount the given middleware at a specific path. This will register the
 * middleware for all HTTP methods on the given route, and strip the path
 * from `ctx.path` temporarilly when calling the middleware.
 *
 * Internally, this acts by setting the option `{mount: '*'}` on the generated
 * route while also ensuring the path ends with `/*`. This method will not
 * function correctly if the built-in mount middleware is disabled.
 *
 * @param {String|String[]} path The path to mount the middleware at.
 * @param {Object} [options] An optional set of options for the middleware.
 * @param {...Function} middleware The middleware function(s) to use
 * @returns {Router} The Router
 */
Router.prototype.mount = function(path, options, ...middleware) {
	if ( ! Array.isArray(path) )
		path = [path];

	if ( typeof options === 'function' ) {
		middleware.unshift(options);
		options = null;
	} else if ( typeof options !== 'object' )
		throw new TypeError('options must be an object');

	for(const p of path)
		if ( typeof p !== 'string' )
			throw new TypeError('path must be a string or array of strings');

	for(const fn of middleware)
		if ( typeof fn !== 'function' )
			throw new TypeError('middleware must be a function');

	if ( options )
		options = Object.assign({}, options, {mount: '*'});
	else
		options = {mount: '*'};

	this.register(['all'], path.map(p => {
		if ( p.endsWith('/') )
			return `${p}*`;

		else if ( ! p.endsWith('/*') )
			return `${p}/*`;

		return p;

	}), options, ...middleware);

	return this;
}


/**
 * Nest another {@link Router} as a child of this router, inheriting all of
 * its routes, middleware, etc.
 *
 * @example
 * const users = Router({prefix: '/user'});
 *
 * users.get('/:userID', ctx => {
 *     // ...
 * });
 *
 * router.nest(users);
 *
 * @param {String|String[]} [path] The path to nest the router at.
 * @param {Router} router The router instance to be nested.
 * @returns {Router} The Router
 */
Router.prototype.nest = function(path, router) {
	if ( path instanceof Router ) {
		router = path;
		path = null;
	}

	this._nest(path, router);
	this._update();
	return this;
}

Router.prototype._nest = function(path, router) {
	if ( ! path )
		path = [''];

	if ( ! Array.isArray(path) )
		path = [path];

	for(const p of path)
		if ( typeof p !== 'string' )
			throw new TypeError('path must be a string or array of strings');

	if ( !(router instanceof Router) )
		throw new TypeError('router must be a Router');

	for(const p of path) {
		const nests = this._nested[p] = this._nested[p] || [];
		if ( ! nests.includes(router) )
			nests.push(router);
	}

	if ( ! router._parents.includes(this) )
		router._parents.push(this);
}


/**
 * Generate a URL for the route with the given name.
 *
 * Routes will inherit the name of the {@link Router} that contains them. As
 * a shortcut for accessing other routes in the same namespace, you can start
 * the name passed to `urlFor` with a period.
 *
 * `urlFor` is assigned to the current Koa context and should be used there
 * to ensure namespaces work correctly.
 *
 * Once the URL has been built (using
 * [path-to-regexp](https://github.com/pillarjs/path-to-regexp)) that generated
 * URL and any left over parameters are merged into `options` and the structure
 * is passed to [url.format()](https://nodejs.org/api/url.html#url_url_format_urlobject)
 *
 * If the Router or specific route is using a host, and a host hasn't been specified
 * in options, the host will be checked against `source_host`. If the host does not
 * match, an absolute URL will be generated.
 *
 * Any host variables for the route must be included in `params`.
 *
 * @example
 * const router = Router(),
 *       user_router = Router({name: 'user', prefix: '/user'});
 *
 * user_router.get('me', '/me', ctx => {
 *     ctx.redirect(ctx.urlFor('.id', ctx.state.current_user.id));
 * });
 *
 * user_router.get('id', '/:userID', ctx => {
 *     // ...
 * });
 *
 * router.use(user_router);
 *
 * router.get('/', ctx => {
 *     ctx.redirect(ctx.urlFor('user.me'));
 * });
 *
 * @param {String} name The name of the route
 * @param {Object} [params] Parameters to place in the generated URL.
 * Required if the route takes parameters. Any parameter not consumed in the route
 * will be added as a query parameter.
 * @param {Object} [options] Options to pass to `url.format()`.
 * @param {Object} [options.query] Query parameters for the generated URL.
 * @param {Object} [options.absolute=false] If set to true, the generated URL will always be absolute.
 * @param {String} [source_host] The host from the request that triggered this method.
 * @param {String} [source_protocol] The protocol from the request that triggered this method.
 * @returns {String} The generated URL.
 */
Router.prototype.urlFor = function(name, params = {}, options = {}, source_host, source_protocol) {
	if ( ! this._named[name] )
		throw new Error('No such named route');

	if ( options == null )
		options = {};
	else if ( typeof options !== 'object' )
		throw new TypeError('Invalid options for urlFor: must be object');

	const [known_params, fn, host_rich, host, host_vars, host_fn] = this._named[name],
		query = options.query = options.query || {};

	if ( params == null )
		params = {};
	else if ( typeof params !== 'object' )
		throw new TypeError('Invalid parameters for urlFor: must be object');

	for(const [name, val] of Object.entries(params)) {
		if ( ! known_params.includes(name) && !( host_vars && host_vars.includes(name)) )
			query[name] = val;
	}

	if ( ! options.host ) {
		let url_host;
		if ( host_rich && host_fn )
			url_host = host_fn(params);
		else if ( ! host_rich && host )
			url_host = host;

		if ( ! url_host )
			url_host = this.opts.host || source_host;

		if ( url_host && (options.absolute || url_host !== source_host) ) {
			options.absolute = undefined;
			options.host = url_host;

			if ( options.slashes == null )
				options.slashes = true;

			if ( options.slashes && options.protocol == null && source_protocol )
				options.protocol = source_protocol;
		}
	}

	options.pathname = fn(params);
	return formatURL(options);
}


Router.Mount = Mount;
Router.compose = compose;