camelaissani/frontexpress

View on GitHub
lib/router.js

Summary

Maintainability
C
7 hrs
Test Coverage
/**
 * Module dependencies.
 * @private
 */

import HTTP_METHODS from './methods';
import {toParameters} from './application';
import Middleware from './middleware';


/**
 * Route object.
 * @private
 */

export class Route {


    /**
     * Initialize the route.
     *
     * @private
     */

    constructor(router, uriPart, method, middleware) {
        this.router = router;
        this.uriPart = uriPart;
        this.method = method;
        this.middleware = middleware;
        this.visited = false;
    }


    /**
     * Return route's uri.
     *
     * @private
     */

    get uri() {
        if (!this.uriPart && !this.method) {
            return undefined;
        }

        if (this.uriPart instanceof RegExp) {
            return this.uriPart;
        }

        if (this.router.baseUri instanceof RegExp) {
            return this.router.baseUri;
        }

        if (this.router.baseUri) {
            const baseUri = this.router.baseUri.trim();
            if (this.uriPart) {
                return ( baseUri + this.uriPart.trim()).replace(/\/{2,}/, '/');
            }
            return baseUri;
        }

        return this.uriPart;
    }
}


/**
 * Router object.
 * @public
 */

const error_middleware_message = 'method takes at least a middleware';
export default class Router {


    /**
     * Initialize the router.
     *
     * @private
     */

    constructor(uri) {
        this._baseUri = uri;
        this._routes = [];
    }


    /**
     * Do some checks and set _baseUri.
     *
     * @private
     */

    set baseUri(uri) {
        if (!uri) {
            return;
        }

        if (!this._baseUri) {
            this._baseUri = uri;
            return;
        }

        if (typeof this._baseUri !== typeof uri) {
            throw new TypeError('router cannot mix regexp and uri');
        }
    }


    /**
     * Return router's _baseUri.
     *
     * @private
     */

    get baseUri() {
        return this._baseUri;
    }


    /**
     * Add a route to the router.
     *
     * @private
     */

    _add(route) {
        this._routes.push(route);
        return this;
    }


    /**
     * Gather routes from routers filtered by _uri_ and HTTP _method_.
     *
     * @private
     */

    routes(application, request) {
        request.params = request.params || {};
        const isRouteMatch = application.get('route matcher');
        return this._routes.filter((route) => {
            return isRouteMatch(request, route);
        });
    }


    /**
     * Gather visited routes from routers.
     *
     * @private
     */

    visited() {
        return this._routes.filter(route => route.visited);
    }


    /**
     * Use the given middleware function or object on this router.
     *
     *    // middleware function
     *    router.use((req, res, next) => {console.log('Hello')});
     *
     *    // middleware object
     *    router.use(new Middleware());
     *
     * @param {Middleware|Function} middleware object or function
     * @return {Router} for chaining
     *
     * @public
     */

    use(middleware) {
        if (!(middleware instanceof Middleware) && (typeof middleware !== 'function') ) {
            throw new TypeError(error_middleware_message);
        }

        this._add(new Route(this, undefined, undefined, middleware));

        return this;
    }


    /**
     * Use the given middleware function or object on this router for
     * all HTTP methods.
     *
     *    // middleware function
     *    router.all((req, res, next) => {console.log('Hello')});
     *
     *    // middleware object
     *    router.all(new Middleware());
     *
     * @param {Middleware|Function} middleware object or function
     * @return {Router} for chaining
     *
     * @public
     */

    all(...args) {
        const {middleware} = toParameters(args);
        if (!middleware) {
            throw new TypeError(error_middleware_message);
        }

        HTTP_METHODS.forEach((method) => {
            this[method.toLowerCase()](...args);
        });
        return this;
    }
}

HTTP_METHODS.forEach((method) => {


    /**
     * Use the given middleware function or object, with optional _uri_ on
     * HTTP methods: get, post, put, delete...
     * Default _uri_ is "/".
     *
     *    // middleware function will be applied on path "/"
     *    router.get((req, res, next) => {console.log('Hello')});
     *
     *    // middleware object will be applied on path "/" and
     *    router.get(new Middleware());
     *
     *    // middleware function will be applied on path "/user"
     *    router.post('/user', (req, res, next) => {console.log('Hello')});
     *
     *    // middleware object will be applied on path "/user" and
     *    router.post('/user', new Middleware());
     *
     * @param {String} uri
     * @param {Middleware|Function} middleware object or function
     * @return {Router} for chaining
     * @public
     */

    const methodName = method.toLowerCase();
    Router.prototype[methodName] = function(...args) {
        const {baseUri, middleware} = toParameters(args);
        if (!middleware) {
            throw new TypeError(error_middleware_message);
        }

        if (baseUri && this._baseUri && this._baseUri instanceof RegExp) {
            throw new TypeError('router cannot mix uri/regexp');
        }

        this._add(new Route(this, baseUri, method, middleware));

        return this;
    };
});

export function routeMatcher(request, route) {
    // check if http method are equals
    if (route.method && route.method !== request.method) {
        return false;
    }


    // route and uri not defined always match
    if (!route.uri || !request.uri) {
        return true;
    }

    //remove query string and anchor from uri to test
    const match = /^(.*)\?.*#.*|(.*)(?=\?|#)|(.*[^\?#])$/.exec(request.uri);
    const baseUriToCheck = match[1] || match[2] || match[3];

    // if route is a regexp path
    if (route.uri instanceof RegExp) {
        return baseUriToCheck.match(route.uri) !== null;
    }

    // if route is parameterized path
    if (route.uri.indexOf(':') !== -1) {

        const decodeParmeterValue = (v) => {
            return !isNaN(parseFloat(v)) && isFinite(v) ? (Number.isInteger(v) ? Number.parseInt(v, 10) : Number.parseFloat(v)) : v;
        };

        // figure out key names
        const keys = [];
        const keysRE = /:([^\/\?]+)\??/g;
        let keysMatch = keysRE.exec(route.uri);
        while (keysMatch != null) {
            keys.push(keysMatch[1]);
            keysMatch = keysRE.exec(route.uri);
        }

        // change parameterized path to regexp
        const regExpUri = route.uri
        //                             :parameter?
                            .replace(/\/:[^\/]+\?/g, '(?:\/([^\/]+))?')
        //                             :parameter
                            .replace(/:[^\/]+/g, '([^\/]+)')
        //                             escape all /
                            .replace('/', '\\/');

        // checks if uri match
        const routeMatch = baseUriToCheck.match(new RegExp(`^${regExpUri}$`));
        if (!routeMatch) {
            return false;
        }

        // update params in request with keys
        request.params = Object.assign(request.params, keys.reduce((acc, key, index) => {
            let value = routeMatch[index + 1];
            if (value) {
                value = value.indexOf(',') !== -1 ? value.split(',').map(v => decodeParmeterValue(v)) : value = decodeParmeterValue(value);
            }
            acc[key] = value;
            return acc;
        }, {}));
        return true;
    }

    // if route is a simple path
    return route.uri === baseUriToCheck;
}