Home Manual Reference Source Repository

lib/index.js

const env = require('./env');
const http = require('http');
const Request = require('./Request');
const Response = require('./Response');
const UriTemplate = require('./UriTemplate');
const url = require('url');

const noop = () => {};

module.exports = class Bq {
    constructor() {
        this.routes = {};

        const defaults = {
            BQ_DEFAULT_CHARSET: 'utf-8',
            BQ_RUN_DEFAULT_PORT: 8080,
            BQ_RUN_DISPLAY_MSG: true
        };

        Object.entries(defaults).forEach(pair => {
            const [name, value] = pair;

            if (env(name) === undefined) {
                env(name, value);
            }
        });
    }

    /**
     * A helper method to work with the instance's internal `#routes` property.
     * Acts as a getter if the `route` parameter is not specified and a setter
     * if it is. This method always returns the specified route object.
     *
     * @param {string} path - The path name to get/set the route _(e.g. `'/resource'`)_.
     * @param {Route} [route] - An object consisting of callback functions mapped to method keys.
     * @return {RouteHandler}
     */
    route(path, route) {
        if (route) {
            this.routes[path] = route;
        }

        return Object.keys(this.routes).reduce((accumulator, routeId) => {
            if (!new UriTemplate(routeId).test(path)) {
                return accumulator;
            }

            return this.routes[routeId];
        }, noop);
    }

    /**
     * Start the app server.
     *
     * @param {Object} [settings={port: 8080}] - The port setting defaults to the value of the `BQ_RUN_DEFAULT_PORT` envvar.
     * @return {http.Server} The server instance that has been started.
     * @see https://nodejs.org/api/http.html#http_class_http_server
     */
    run(settings = {}) {
        const defaults = {port: env('BQ_RUN_DEFAULT_PORT')};
        const {port} = Object.assign(defaults, settings);
        const uri = `http://localhost:${port}/`;
        const server = http.createServer(this.serve.bind(this));

        if (env('BQ_RUN_DISPLAY_MSG')) {
            console.log(`Now listening at <${uri}>.`);
        }

        return server.listen(port);
    }

    /**
     * The callback invoked by `#run()` when the server receives a client request.
     *
     * @param {http.IncomingMessage} incomingMessage
     * @param {http.ServerResponse} serverResponse
     * @return {undefined}
     * @see https://nodejs.org/api/http.html#http_class_http_incomingmessage
     * @see https://nodejs.org/api/http.html#http_class_http_serverresponse
     * @see https://nodejs.org/api/http.html#http_response_end_data_encoding_callback
     */
    serve(incomingMessage, serverResponse) {
        let request = new Request(incomingMessage);
        let response = new Response(serverResponse);
        let uriTemplate = Object.keys(this.routes).reduce((accumulator, routeId) => {
            let uriTemplate = new UriTemplate(routeId);

            if (!uriTemplate.test(request.path)) {
                return accumulator;
            }

            return uriTemplate;
        }, new UriTemplate());
        let route = this.route(request.path);
        let method = request.method;
        request.uriVariables = uriTemplate.getVariables(request.path);

        if (!route || !route[method]) {
            Object.assign(response, {statusCode: 404, text: 'Resource not found.'});
            response.send();

            return;
        }

        const callback = route[method];

        /** @see https://stackoverflow.com/questions/38508420/how-to-know-if-a-function-is-async#answer-38510353 */
        if (callback[Symbol.toStringTag] === 'AsyncFunction') {
            callback(request, response).catch(error => {
                Object.assign(response, {statusCode: 500, text: JSON.stringify(error)});
            }).then(() => {
                response.send();
            });
        } else {
            callback(request, response);
            response.send();
        }
    }
};