Project creation

boostrap: start webserver, router catches requests
routeur: create outputter and endpoint, trigger actions
outputter: write in specified format
action: act on resource
This commit is contained in:
Meutel 2014-01-19 14:37:31 +01:00
commit 0d0f564475
11 changed files with 520 additions and 0 deletions

4
.gitignore vendored Normal file
View File

@ -0,0 +1,4 @@
*.orig
*.rej
*~
*.*.swp

57
bin/bootstrap Executable file
View File

@ -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);
}

3
config/config.json Normal file
View File

@ -0,0 +1,3 @@
{
"httpPort":8080
}

96
lib/action/action.js Normal file
View File

@ -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
};

35
lib/bouquins.js Normal file
View File

@ -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;
}

51
lib/endpoint/endpoint.js Normal file
View File

@ -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 <string> HTTP method
* @param url <url> target URL
* @param callback <function(err, action)> 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;

View File

@ -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
};

121
lib/router/router.js Normal file
View File

@ -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 <string> path
* @param callback <function(error, endpoint)> 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 <string> requested mime type.
* @param callback <function(err)> 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();
});
});
}
};

22
lib/util/config.js Normal file
View File

@ -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);
}
});
};

28
lib/util/logger.js Normal file
View File

@ -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);
}

10
package.json Normal file
View File

@ -0,0 +1,10 @@
{
"name" : "bouquins",
"description" : "HTTP frontend for calibre",
"url" : "TODO",
"author" : "Meutel <meutel@meutel.net>",
"dependencies" : {},
"main" : "./lib/bouquins",
"bin" : { "bootstrap" : "./bin/bootstrap" },
"version" : "0.1.0"
}