UNPKG

@akala/core

Version:
769 lines (644 loc) 20.4 kB
/*! * 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 // } // })