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:
commit
0d0f564475
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
*.orig
|
||||
*.rej
|
||||
*~
|
||||
*.*.swp
|
57
bin/bootstrap
Executable file
57
bin/bootstrap
Executable 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
3
config/config.json
Normal file
@ -0,0 +1,3 @@
|
||||
{
|
||||
"httpPort":8080
|
||||
}
|
96
lib/action/action.js
Normal file
96
lib/action/action.js
Normal 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
35
lib/bouquins.js
Normal 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
51
lib/endpoint/endpoint.js
Normal 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;
|
93
lib/outputter/outputter.js
Normal file
93
lib/outputter/outputter.js
Normal 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
121
lib/router/router.js
Normal 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
22
lib/util/config.js
Normal 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
28
lib/util/logger.js
Normal 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
10
package.json
Normal 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"
|
||||
}
|
Loading…
Reference in New Issue
Block a user