UNPKG

nocca

Version:
223 lines (158 loc) 6.84 kB
'use strict'; var $express = require('express'); var $http = require('http'); var $url = require('url'); var $urlPattern = require('url-pattern'); var $extend = require('extend'); module.exports = HttpApi; function HttpApi (Nocca) { var self = this; self.logger = Nocca.logger.child({ module: 'HttpApi' }); // Setup the HTTP server on instantiation and use the request router to handle all traffic self.router = $express.Router(); self.router.use(createRequestRouter(Nocca.config)); // Configured routes will be collected in this map var routes = { direct: {}, pattern: [], help: [] }; // wait for all config to have rendered before continuing Nocca.pubsub.subscribe(Nocca.constants.PUBSUB_NOCCA_INITIALIZE_PLUGIN, init); // Listen to additional routes being added by loaded plugins Nocca.pubsub.subscribe(Nocca.constants.PUBSUB_REST_ROUTE_ADDED, addRoute); function init () { // Configure routes addRoute('GET:/routes', getRoutes); } // Note that a handler configured with addRoute can get up to 4 parameters: // req - active request // res - active response // config - active Nocca config // match - match information on route (e.g. parameter values) // Wraps the request router in a closure to provide access to the configuration function createRequestRouter (config) { var router = requestRouter; router.config = config; return router; // Selects handlers from the routes map (routes are defined below) function requestRouter (req, res) { var route = req.method.toUpperCase() + ':' + $url.parse(req.url).pathname; self.logger.debug('Checking route: ' + route); if (routes.direct.hasOwnProperty(route)) { invokeRoute(routes.direct[route], req, res); } else { var match; var handler; for (var idx = 0; idx < routes.pattern.length && !match; idx++) { match = routes.pattern[idx].match(route); if (match) { handler = routes.pattern[idx].handler; } } if (typeof handler !== 'undefined') { invokeRoute(handler, req, res, match); } else { res.writeHead(404, 'Not found', { 'Access-Control-Allow-Origin': '*' }); res.write('Could not open ' + req.url, function () { res.end(); }); } } } } function invokeRoute (handler, req, res, matches) { handler(new ApiRequest(req, res, matches)); } // --- Route definitions // Adds a handler to the routes map using one or more route definitions (first argument can be an array) // Route definitions are of the form METHOD:/p/a/t/h // Further specialization on query parameters or headers is not provided function addRoute (routeStrings, isPattern, handler) { // convert arguments to array var args = Array.prototype.slice.call(arguments); // always first arg, extract routeStrings = args.shift(); isPattern = typeof args[0] === 'boolean' ? args.shift() : false; handler = args.shift(); var helpText = args.length ? args.shift() : undefined; self.logger.debug('Adding route to REST api: ' + routeStrings); // force routeStrings to array if (!Array.isArray(routeStrings)) { routeStrings = [routeStrings]; } routeStrings.forEach(function (routeDefinition) { if (isPattern) { var p = $urlPattern.newPattern(routeDefinition); p.handler = handler; routes.pattern.push(p); } else { routes.direct[routeDefinition] = handler; } // add route to help for reference var routeParts = routeDefinition.split(/:/); routes.help.push({ method: routeParts[0], path: routeParts[1], description: helpText }); }); } function getRoutes (apiReq) { apiReq.ok().end(routes.help); } function ApiRequest (req, res, matches) { this.req = req; this.res = res; this.matches = matches; this.headWritten = false; } ApiRequest.prototype.nocca = function () { return Nocca; }; ApiRequest.prototype.writeHead = function (statusCode, statusMessage, headers) { var writeHeaders = { 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Methods': 'GET,PUT,DELETE', 'Access-Control-Allow-Headers': 'content-type' }; statusCode = statusCode || 200; statusMessage = statusMessage || $http.STATUS_CODES[statusCode]; if (typeof headers === 'undefined' && typeof statusMessage === 'object') { headers = statusMessage; statusMessage = $http.STATUS_CODES[statusCode]; } if (typeof headers === 'object') { // add headers for cross domain access writeHeaders = $extend(true, {}, writeHeaders, headers); } this.res.writeHead(statusCode, statusMessage, writeHeaders); this.headWritten = true; return this; }; ApiRequest.prototype.end = function (data, noJson) { // check if data is an object and not null if (data && !noJson) { // stringify! what a convenience. // pretty print? var parsedUrl = $url.parse(this.req.url, true); if (parsedUrl.query.jsonPrettyPrint === 'true') { data = JSON.stringify(data, null, 4); } else { data = JSON.stringify(data); } } this.res.end(data); }; ApiRequest.prototype.ok = function (message, headers) { return this.writeHead(200, message, headers); }; ApiRequest.prototype.moved = function (message, headers) { return this.writeHead(301, message, headers); }; ApiRequest.prototype.badRequest = function (message, headers) { return this.writeHead(400, message, headers); }; ApiRequest.prototype.notFound = function (message, headers) { return this.writeHead(404, message, headers); }; ApiRequest.prototype.conflict = function (message, headers) { return this.writeHead(409, message, headers); }; ApiRequest.prototype.internalError = function (message, headers) { return this.writeHead(500, message, headers); }; }