@akala/core
Version:
769 lines (644 loc) • 20.4 kB
text/typescript
/*!
* router
* Copyright(c) 2013 Roman Shtylman
* Copyright(c) 2014 Douglas Christopher Wilson
* MIT Licensed
*/
/**
* Module dependencies.
* @private
*/
var debug = require('debug')('router')
import { Layer, LayerOptions } from './layer';
// import * as methods from 'methods';
import { extend } from '../helpers';
import { parse as parseUrl } from 'url';
import { Route, IRoutable } from './route';
import * as http from 'http'
export { Layer, Route, LayerOptions, IRoutable };
export type RoutableLayer<T extends Function> = Layer<T> & IRoutable<T>;
var slice = Array.prototype.slice
/* istanbul ignore next */
var defer = typeof setImmediate === 'function'
? setImmediate
: function (fn, ...args) { process.nextTick(fn.bind.apply(fn, arguments)) }
export interface RouterOptions
{
caseSensitive?: boolean;
mergeParams?: boolean;
strict?: boolean;
length?: number;
separator?: string;
}
export interface NextParamCallback
{
(error): void;
(): void | any;
}
export type ParamCallback = (req, paramCallback: NextParamCallback, paramVal: any, name: string, ...rest) => void;
export interface Request
{
next?: NextFunction;
baseUrl?: string;
url?: string;
params?: { [key: string]: any };
originalUrl?: string;
route?: Route<any, Layer<any>>
}
export interface NextFunction
{
(arg: 'router'): void;
(arg: 'route'): void;
(err: any): void;
(): void;
}
export type Middleware1<T extends Request> = (req: T, next: NextFunction) => void;
export type Middleware2<T extends Request, U> = (req: T, res: U, next: NextFunction) => void;
export type ErrorMiddleware1<T extends Request> = (error: any, req: T, next: NextFunction) => void;
export type ErrorMiddleware2<T extends Request, U> = (error: any, req: T, res: U, next: NextFunction) => void;
export type Middleware1Extended<T extends Request> = Middleware1<T> | ErrorMiddleware1<T>;
export type Middleware2Extended<T extends Request, U> = Middleware2<T, U> | ErrorMiddleware2<T, U>;
export abstract class Router<T extends (Middleware1<any> | Middleware2<any, any>), U extends (ErrorMiddleware1<any> | ErrorMiddleware2<any, any>), TLayer extends (Layer<T> & IRoutable<T>), TRoute extends Route<T, TLayer>>
{
constructor(options?: RouterOptions)
{
var opts = options || {}
this.caseSensitive = opts.caseSensitive
this.mergeParams = opts.mergeParams;
this.separator = opts.separator || '/';
this.strict = opts.strict
this.length = opts.length || 2;
}
private separator: string;
private length: number;
private caseSensitive: boolean;
private mergeParams: boolean;
private params: { [param: string]: ParamCallback[] } = {}
private strict: boolean;
private stack = []
public router = this.handle.bind(this);
/**
* Map the given param placeholder `name`(s) to the given callback.
*
* Parameter mapping is used to provide pre-conditions to routes
* which use normalized placeholders. For example a _:user_id_ parameter
* could automatically load a user's information from the database without
* any additional code.
*
* The callback uses the same signature as middleware, the only difference
* being that the value of the placeholder is passed, in this case the _id_
* of the user. Once the `next()` function is invoked, just like middleware
* it will continue on to execute the route, or subsequent parameter functions.
*
* Just like in middleware, you must either respond to the request or call next
* to avoid stalling the request.
*
* router.param('user_id', function(req, res, next, id){
* User.find(id, function(err, user){
* if (err) {
* return next(err)
* } else if (!user) {
* return next(new Error('failed to load user'))
* }
* req.user = user
* next()
* })
* })
*
* @param {string} name
* @param {function} fn
* @public
*/
public param(name: string, fn: ParamCallback)
{
if (!name)
{
throw new TypeError('argument name is required')
}
if (typeof name !== 'string')
{
throw new TypeError('argument name must be a string')
}
if (!fn)
{
throw new TypeError('argument fn is required')
}
if (typeof fn !== 'function')
{
throw new TypeError('argument fn must be a function')
}
var params = this.params[name]
if (!params)
{
params = this.params[name] = []
}
params.push(fn)
return this
}
/**
* Dispatch a req, res into the router.
*
* @private
*/
public handle<TRequest extends Request>(req: TRequest, ...rest)
{
return this.internalHandle.apply(this, [{}, req].concat(rest));
}
protected internalHandle(options, req, ...rest)
{
var callback = rest[rest.length - 1];
if (options && !options.ensureCleanStart)
{
options.ensureCleanStart = function ()
{
if (req.url[0] !== separator)
{
req.url = separator + req.url
slashAdded = true
}
};
}
if (!callback)
{
throw new TypeError('argument callback is required')
}
debug('dispatching %s %s', req['method'] || '', req.url)
var idx = 0
var removed = ''
var self = this
var slashAdded = false
var paramcalled = {};
var separator = this.separator;
// middleware and routes
var stack = this.stack
// manage inter-router variables
var parentParams = req.params
var parentUrl: string = req.baseUrl || '';
var done = Router.restore(callback, req, 'baseUrl', 'next', 'params')
// setup next layer
req.next = next
if (options && options.preHandle)
{
done = options.preHandle(done);
}
// setup basic req values
req.baseUrl = parentUrl
req.originalUrl = req.originalUrl || req.url
next()
function next(err?)
{
var layerError = err === 'route'
? null
: err
// remove added slash
if (slashAdded && req.url)
{
req.url = req.url.substr(1)
slashAdded = false
}
// restore altered req.url
if (removed.length !== 0)
{
self.unshift(req, removed, parentUrl);
removed = '';
}
// signal to exit router
if (layerError === 'router')
{
defer(done, null)
return
}
// no more matching layers
if (idx >= stack.length)
{
defer(done, layerError)
return
}
// get pathname of request
var path = self.getPathname(req)
if (path == null)
{
return done(layerError)
}
// find next matching layer
var layer: TLayer;
var match: boolean;
var route: TRoute
while (match !== true && idx < stack.length)
{
layer = stack[idx++]
match = Router.matchLayer(layer, path)
route = <TRoute>layer.route
if (typeof match !== 'boolean')
{
// hold on to layerError
layerError = layerError || match
}
if (match !== true)
{
continue
}
if (!route)
{
// process non-route handlers normally
continue
}
if (layerError)
{
// routes do not match with a pending error
match = false
continue
}
var isApplicable = route.isApplicable(req)
// build up automatic options response
if (!isApplicable)
{
if (options && options.notApplicableRoute)
{
if (options.notApplicableRoute(route) === false)
{
match = false
continue
}
}
}
}
// no match
if (match !== true)
{
return done(layerError)
}
// store route for dispatch on change
if (route)
{
req.route = route
}
// Capture one-time layer values
req.params = self.mergeParams
? Router.mergeParams(layer.params, parentParams)
: layer.params
var layerPath = layer.path
var args: any[] = [req];
args = args.concat(rest.slice(0, rest.length - 1));
;
// this should be done for the layer
self.process_params.apply(self, [layer, paramcalled].concat(args).concat(function (err)
{
if (err)
{
return next(layerError || err)
}
if (route)
{
return layer.handle_request.apply(layer, args.concat(next));
}
trim_prefix(layer, layerError, layerPath, path)
}));
}
function trim_prefix(layer: TLayer, layerError, layerPath: string, path: string)
{
if (layerPath.length !== 0)
{
// Validate path breaks on a path separator
var c = path[layerPath.length]
if (c && c !== separator)
{
next(layerError)
return
}
// Trim off the part of the url that matches the route
// middleware (.use stuff) needs to have the path stripped
debug('trim prefix (%s) from url %s', layerPath, req.url)
removed = layerPath
self.shift(req, removed);
// Ensure leading slash
options.ensureCleanStart(req);
// Setup base URL (no trailing slash)
req.baseUrl = parentUrl + (removed[removed.length - 1] === separator
? removed.substring(0, removed.length - 1)
: removed)
}
debug('%s %s : %s', layer.name, layerPath, req.originalUrl)
var args: any[] = [req].concat(rest.slice(0, rest.length - 1));
args.push(next);
if (layerError)
{
layer.handle_error.apply(layer, [layerError].concat(args))
} else
{
layer.handle_request.apply(layer, args);
}
}
}
protected shift(req, removed)
{
req.url = req.url.substring(removed.length);
}
protected unshift(req, removed, parentUrl)
{
req.baseUrl = parentUrl;
req.url = removed + req.url;
}
public process_params(layer: TLayer, called, req, ...rest)
{
var done = rest[rest.length - 1];
var params = this.params
// captured parameters from the layer, keys and values
var keys = layer.keys
// fast track
if (!keys || keys.length === 0)
{
return done()
}
var i = 0
var name
var paramIndex = 0
var key
var paramVal
var paramCallbacks: ParamCallback[];
var paramCalled: {
error: any,
match: any,
value: any
};
// process params in order
// param callbacks can be async
function param(err?)
{
if (err)
{
return done(err)
}
if (i >= keys.length)
{
return done()
}
paramIndex = 0
key = keys[i++]
name = key.name
paramVal = req.params[name]
paramCallbacks = params[name]
paramCalled = called[name]
if (paramVal === undefined || !paramCallbacks)
{
return param()
}
// param previously called with same value or error occurred
if (paramCalled && (paramCalled.match === paramVal
|| (paramCalled.error && paramCalled.error !== 'route')))
{
// restore value
req.params[name] = paramCalled.value
// next param
return param(paramCalled.error)
}
called[name] = paramCalled = {
error: null,
match: paramVal,
value: paramVal
}
paramCallback()
}
// single param callbacks
function paramCallback(err?)
{
var fn = paramCallbacks[paramIndex++]
// store updated value
paramCalled.value = req.params[key.name]
if (err)
{
// store error
paramCalled.error = err
param(err)
return
}
if (!fn)
return param()
try
{
fn(req, paramCallback, paramVal, key.name, rest.slice(0, rest.length - 1));
} catch (e)
{
paramCallback(e)
}
}
param()
}
/**
* Use the given middleware function, with optional path, defaulting to "/".
*
* Use (like `.all`) will run for any http METHOD, but it will not add
* handlers for those methods so OPTIONS requests will not consider `.use`
* functions even if they could respond.
*
* The other difference is that _route_ path is stripped and not visible
* to the handler function. The main effect of this feature is that mounted
* handlers can operate without any code changes regardless of the "prefix"
* pathname.
*
* @public
*/
public use(...handlers: (T | U)[])
public use(path: string, ...handlers: (T | U)[])
public use(...handlers: (string | T | U)[])
{
var offset = 0
var path = this.separator;
// default path to *separator*
// disambiguate router.use([handler])
if (typeof handlers[0] !== 'function')
{
// first arg is the path
if (typeof handlers[0] == 'string')
{
offset = 1
path = <string>handlers.shift();
}
}
var callbacks = handlers as Array<T | U>
if (callbacks.length === 0)
{
throw new TypeError('argument handler is required')
}
for (var i = 0; i < callbacks.length; i++)
{
this.layer(path, callbacks[i]);
}
return this
}
protected layer(path: string, fn: T | U)
{
if (typeof fn !== 'function')
{
throw new TypeError('argument handler must be a function')
}
// add the middleware
debug('use %o %s', path, fn.name || '<anonymous>')
var layer = this.buildLayer(path, {
sensitive: this.caseSensitive,
strict: false,
end: false,
length: this.length
}, fn)
layer.route = undefined
this.stack.push(layer)
return layer;
}
protected abstract buildLayer(path: string, options: LayerOptions, handler: T | U): TLayer;
protected abstract buildRoute(path: string): TRoute;
/**
* Create a new Route for the given path.
*
* Each route contains a separate middleware stack and VERB handlers.
*
* See the Route api documentation for details on adding handlers
* and middleware to routes.
*
* @param {string} path
* @return {Route}
* @public
*/
public route(path: string): TRoute
{
var route = this.buildRoute(path)
var layer = this.buildLayer(path, {
sensitive: this.caseSensitive,
strict: this.strict,
end: true,
length: this.length
}, route.dispatch.bind(route))
layer.route = route
this.stack.push(layer)
return route
}
/**
* Get pathname of request.
*
* @param {IncomingMessage} req
* @private
*/
public getPathname(req: any)
{
try
{
return parseUrl(req.url).pathname;
}
catch (err)
{
return undefined;
}
}
/**
* Match path to a layer.
*
* @param {Layer} layer
* @param {string} path
* @private
*/
protected static matchLayer<T extends Function>(layer: Layer<T>, path: string)
{
try
{
return layer.match(path);
} catch (err)
{
return err;
}
}
/**
* Merge params with parent params
*
* @private
*/
protected static mergeParams(params, parent)
{
if (typeof parent !== 'object' || !parent)
{
return params
}
// make copy of parent for base
var obj = extend({}, parent)
// simple non-numeric merging
if (!(0 in params) || !(0 in parent))
{
return extend(obj, params)
}
var i = 0
var o = 0
// determine numeric gap in params
while (i in params)
{
i++
}
// determine numeric gap in parent
while (o in parent)
{
o++
}
// offset numeric indices in params before merge
for (i--; i >= 0; i--)
{
params[i + o] = params[i]
// create holes for the merge when necessary
if (i < o)
{
delete params[i]
}
}
return extend(obj, params)
}
protected static restore(fn, obj, ...props: string[])
{
var vals = new Array(arguments.length - 2)
for (var i = 0; i < props.length; i++)
{
vals[i] = obj[props[i]]
}
return function (...args)
{
// restore vals
for (var i = 0; i < props.length; i++)
{
obj[props[i]] = vals[i]
}
return fn.apply(this, arguments)
}
}
protected static wrap(old, fn)
{
return function proxy()
{
var args = new Array(arguments.length + 1)
args[0] = old
for (var i = 0, len = arguments.length; i < len; i++)
{
args[i + 1] = arguments[i]
}
fn.apply(this, args)
}
}
}
export abstract class Router1<T extends Request, TLayer extends RoutableLayer<Middleware1<T>>, TRoute extends Route<Middleware1<T>, TLayer>> extends Router<Middleware1<T>, ErrorMiddleware1<T>, TLayer, TRoute>
{
constructor(options?: RouterOptions)
{
super(options);
}
}
export abstract class Router2<T extends Request, U, TLayer extends RoutableLayer<Middleware2<T, U>>, TRoute extends Route<Middleware2<T, U>, TLayer>> extends Router<Middleware2<T, U>, ErrorMiddleware2<T, U>, TLayer, TRoute>
{
constructor(options?: RouterOptions)
{
super(options);
}
}
// // create Router#VERB functions
// methods.concat('all').forEach(function (method)
// {
// Router.prototype[method] = function (path)
// {
// var route = this.route(path)
// route[method].apply(route, slice.call(arguments, 1))
// return this
// }
// })