commit 0d0f5644751aacd85438b7441c0c7b316e4e0e9d Author: Meutel Date: Sun Jan 19 14:37:31 2014 +0100 Project creation boostrap: start webserver, router catches requests routeur: create outputter and endpoint, trigger actions outputter: write in specified format action: act on resource diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..de8d62b --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +*.orig +*.rej +*~ +*.*.swp diff --git a/bin/bootstrap b/bin/bootstrap new file mode 100755 index 0000000..9801c2f --- /dev/null +++ b/bin/bootstrap @@ -0,0 +1,57 @@ +#!/usr/bin/env nodejs +/** + * Bouquins bootstrap. + * TODO license + * + * Load configuration + * Check configuration + * Launch http server + * Initialise router + */ +APP_NAME='Bouquins'; +DEFAULT_CONF='./config/config.json'; + +var bouquins = require('../lib/bouquins'); + +console.log('Bootstraping ' + APP_NAME); + +// TODO argument conf file +var configfile = DEFAULT_CONF; +bouquins.loadconfig(configfile, function(err, config) { + if (err) { + console.error('Fatal error, cannot load config: ' + err); + console.log(err.stack); + process.exit(1); + } + // global config + GLOBAL.config = config; + // global logger + GLOBAL.logger = bouquins.initLogger(); + start(config); +}); + +/** + * Initialise application + */ +function start(config){ + logger.info('Starting '+APP_NAME); + + var starReqListener=function(req, res) { + // defaut request listener + res.writeHead(200, {'Content-Type': 'text/plain'}); + res.end(APP_NAME + ' is starting'); + } + // launch http server, wait config and router + var server = require('http').createServer(starReqListener) + .listen(config.httpPort); + logger.info('Listening on '+config.httpPort); + + // TODO check config: files/dir do exist, database exist, can read ... + + // init router + var router = bouquins.makeRouter(); + server.on('request', function(req, resp) { + router.request(req, resp); + }).removeListener('request', starReqListener); +} + diff --git a/config/config.json b/config/config.json new file mode 100644 index 0000000..caa6943 --- /dev/null +++ b/config/config.json @@ -0,0 +1,3 @@ +{ + "httpPort":8080 +} diff --git a/lib/action/action.js b/lib/action/action.js new file mode 100644 index 0000000..7d33bcd --- /dev/null +++ b/lib/action/action.js @@ -0,0 +1,96 @@ +/** +* Action. +*/ + +var EventEmitter = require('events').EventEmitter; +var util = require('util'); + +/** +* Constructor. +*/ +function Action(name) { + logger.debug('new Action'); + EventEmitter.call(this); + // + // ATTRIBUTES + // + this.name = name; + + // + // INIT + // +}; +// injerits EventEmitter +Action.prototype = Object.create(EventEmitter.prototype, { + + // + // FUNCTIONS + // + + /** + * @return action needs request body data. + */ + withReqData: { + value: function() { + return false; + }, + enumerable: true, + configurable: true, + writable: true + }, + + /** + * Listen request data event. + */ + reqData: { + value: function(chunk) { + //TODO + }, + enumerable: true, + configurable: true, + writable: true + }, + + /** + * Prepare action. + * @param callback (err, statusCode, headers) + */ + prepare: { + value: function(callback) { + logger.debug('prepare Action'); + // TODO impl per action type + + // default: OK, no headers + callback(null, 200, {}); + }, + enumerable: true, + configurable: true, + writable: true + }, + + doAction: { + value: function(){ + logger.debug('doAction'); + //TODO emit data end events + this.emit('end'); + }, + enumerable: true, + configurable: true, + writable: true + } + +}); + +/** +* Action show single resource. +*/ +function ShowAction() { + logger.debug('new ShowAction'); + Action.call(this, 'show'); +}; +// inherits Action +ShowAction.prototype = Object.create(Action.prototype); + +module.exports = { + ShowAction: ShowAction +}; diff --git a/lib/bouquins.js b/lib/bouquins.js new file mode 100644 index 0000000..bae720b --- /dev/null +++ b/lib/bouquins.js @@ -0,0 +1,35 @@ +/** + * TODO license + * Bouquins module. + */ + +var config = require('./util/config'), + logger = require('./util/logger'), + Router = require('./router/router'), + bouquins = exports; + +var router = null; +/** + * Load config file. + */ +bouquins.loadconfig = function(configfile, callback) { + config.loadconfig(configfile, callback); +}; +/** + * Init logger. + */ +bouquins.initLogger = function() { + if (config.debugLevel) { + logger.debugLevel = config.debugLevel; + } + return logger; +} +/** + * Make main router. + */ +bouquins.makeRouter = function() { + if (!router) { + router = new Router(); + } + return router; +} diff --git a/lib/endpoint/endpoint.js b/lib/endpoint/endpoint.js new file mode 100644 index 0000000..42ff608 --- /dev/null +++ b/lib/endpoint/endpoint.js @@ -0,0 +1,51 @@ +/** + * Endpoint class. + */ +var Action = require('../action/action.js'); + +function Endpoint(path) { + // constructor + this.path = path; +}; +Endpoint.prototype = { + /** + * Build an action on resource endpoint. + * @param method HTTP method + * @param url target URL + * @param callback callback + */ + buildAction : function(method, url, callback) { + var col = this.targetCollection(url.pathname); + var action; + if (col && method == 'POST') { + //TODO search + } else if (col && method == 'GET') { + //TODO list + } else if (!col && method == 'POST') { + //TODO edit + } else if (!col && method == 'GET') { + action = new Action.ShowAction(); + } + if (action) { + this.bind(action, function(err) { + callback(err, action); + }); + } else { + callback(new Error('no action')); + } + }, + + targetCollection : function(pathname) { + // TODO + return false; + }, + + /** + * Bind action to endpoint resource. + */ + bind : function(action, callback) { + //TODO + callback(null, action); + } +}; +exports = module.exports = Endpoint; diff --git a/lib/outputter/outputter.js b/lib/outputter/outputter.js new file mode 100644 index 0000000..ba997c5 --- /dev/null +++ b/lib/outputter/outputter.js @@ -0,0 +1,93 @@ +/** + * TODO license + * Outputter. + */ + +/** + * Outputter class. + * Abstract (not exported). + */ +function Outputter() { + // + // ATTRIBUTES + // + /** + * Output stream. + */ + this.out = null; +}; +Outputter.prototype = { + // + // FUNCTIONS + // + + /** + * Add header to request. + * Available before calling outputTo(). + */ + addHeader: function (name, value) {}, + + /** + * Listen action 'data' event. + * Receive resource instance to output. + */ + output: function (resource) { + //TODO + }, + + /** + * Listen action 'end' event. + * End of action data transmission. + */ + end: function() { + logger.debug('Action ended'); + //TODO + this.out.end(); + }, + + /** + * Set target stream and start outputting. + */ + outputTo: function(stream) { + this.out = stream; + } + +}; + +/** + * Outputter in json format. + */ +var JSONOutputter = function() { + Outputter.call(this); + logger.debug('JSON'); +}; +// inherits Outputter +JSONOutputter.prototype = Object.create(Outputter.prototype, { +}); + +/** + * Outputter in html. + */ +var HtmlOutputter = function() { + Outputter.call(this); +}; +// inherits Outputter +HtmlOutputter.prototype = Object.create(Outputter.prototype, { +}); + +/** + * Outputter in text. + */ +var TextOutputter = function() { + Outputter.call(this); +}; +// inherits Outputter +TextOutputter.prototype = Object.create(Outputter.prototype, { +}); + +//module.exports.JSONOutputter = JSONOutputter; +module.exports = { + JSONOutputter: JSONOutputter, + TextOutputter: TextOutputter, + HtmlOutputter: HtmlOutputter +}; diff --git a/lib/router/router.js b/lib/router/router.js new file mode 100644 index 0000000..7dc0460 --- /dev/null +++ b/lib/router/router.js @@ -0,0 +1,121 @@ +/** +* TODO license +* Router class. +*/ +var util = require('util') + Endpoint = require('../endpoint/endpoint.js'), + Outputter = require('../outputter/outputter'); + +exports = module.exports = Router; +function Router() { + // constructor +} +Router.prototype = { + + // + // FUNCTIONS + // + + /** + * Build the endpoint for given path. + * @param path path + * @param callback callback + */ + buildEndpoint : function (path, callback) { + logger.debug('Building endpoint for ' + path); + // TODO + callback(null, new Endpoint(path)); + }, + + /** + * Build an outputter for given mime type. + * @param mime requested mime type. + * @param callback error callback + */ + buildOutputter : function(mime, callback) { + switch(mime) { + case 'application/json': + return new Outputter.JSONOutputter(); + case 'text/html': + return new Outputter.HtmlOutputter(); + case 'text/plain': + return new Outputter.TextOutputter(); + default: + logger.error('Usupported type: '+mime); + return new Outputter.JSONOutputter(); + } + }, + + /** + * Listener 'request' event. + */ + request: function(req, resp) { + // req headers available + // pause stream until action is ready + req.pause(); + + logger.debug('Handling request'); + + // build outputter + var outputter = this.buildOutputter(req.headers.accept, function(err) { + //TODO error code, terminate resp + }); + + logger.debug('outputter: ' + outputter); + + var url = require('url').parse(req.url, true); + // TODO sanitize url.pathname + this.buildEndpoint(url.pathname, function(err, endpoint) { + //TODO err + //TODO + endpoint.buildAction(req.method, url, function(err, action) { + //TODO err + + // allow outputter to set headers + outputter.addHeader = function(name, value) { + if (resp.headersSent) { + logger.warn('Header already sent, ignoring: ' + name); + } else { + resp.setHeader(name, value); + } + }; + + if (action.withReqData()) { + // action needs all request data + // listen data event + req.on('data', action.reqData); + } + // when request data received, prepare action, send headers, exec action + req.on('end', function() { + action.prepare(function(err, statusCode, headers){ + //TODO err + + // TODO does it keep outputter headers? + resp.writeHead(statusCode, headers); + + // wire streaming event + action.on('data', function(chunk) { + outputter.output(chunk); + }); + action.on('end', function() { + outputter.end(); + }); + + // start outputter + outputter.outputTo(resp); + + // start action + action.doAction(); + + }); + + }); + + // resume reading request stream + req.resume(); + + }); + }); + } + +}; diff --git a/lib/util/config.js b/lib/util/config.js new file mode 100644 index 0000000..69359da --- /dev/null +++ b/lib/util/config.js @@ -0,0 +1,22 @@ +/** + * Config module. + * TODO license + */ + +var config = exports; +/** + * Loads config from config file + */ +config.loadconfig=function(configfile, callback) { + require('fs').readFile( + configfile, {encoding:'utf8'}, + function(err, data) { + if (err) callback(err); + try { + var config = JSON.parse(data); + callback(null, config); + } catch (err) { + callback(err); + } + }); +}; diff --git a/lib/util/logger.js b/lib/util/logger.js new file mode 100644 index 0000000..0b60dc6 --- /dev/null +++ b/lib/util/logger.js @@ -0,0 +1,28 @@ +/** + * TODO license + * Basic logger. + */ +var logger = exports; + +logger.debugLevel = 'debug'; +logger.log = function(level, message) { + var levels = ['fatal', 'error', 'warn', 'info', 'debug']; + if (levels.indexOf(level) <= levels.indexOf(logger.debugLevel) ) { + if (typeof message !== 'string') { + message = JSON.stringify(message); + }; + console.log(new Date().toISOString() + ' [' + level+'] '+message); + } +} +logger.debug = function(message) { + logger.log('debug', message); +} +logger.info = function(message) { + logger.log('info', message); +} +logger.error = function(message) { + logger.log('error', message); +} +logger.fatal = function(message) { + logger.log('fatal', message); +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..4790fdb --- /dev/null +++ b/package.json @@ -0,0 +1,10 @@ +{ + "name" : "bouquins", + "description" : "HTTP frontend for calibre", + "url" : "TODO", + "author" : "Meutel ", + "dependencies" : {}, + "main" : "./lib/bouquins", + "bin" : { "bootstrap" : "./bin/bootstrap" }, + "version" : "0.1.0" +}