test-set/timbits-master/lib/timbits.js
// load required modules
var fs = require('fs')
, path = require('path')
, querystring = require('querystring')
, url = require('url')
, winston = require('winston')
, express = require('express')
, hogan = require('hogan.js')
, request = require('request');
// default configuration
var config = {
appName: 'Timbits',
base: '',
port: 5678,
home: process.cwd(),
maxAge: 60, // default widget output cache time
engine: 'hjs', // default view engine
discovery: true, // support automatic discovery via /timbits/json
help: true, // allow automatic help pages at /timbits/help and /[name]/help
test: true, // allow automatic test pages at /timbits/test and /[name]/test
json: true, // allow built in json view at /[name]/json
jsonp: true // allow jsonp calls via /[name]/json?callback=
};
// retrieve list of matching files in a folder
function filteredFiles(folder, pattern) {
var files = [];
if (fs.existsSync(folder)){
fs.readdirSync(folder).forEach(function(file) {
if (file.match(pattern) != null)
files.push(file);
});
}
return files;
}
// automagically load timbits found in the ./timbits folder
function loadTimbits(callback) {
var folder = path.join(config.home, "/timbits");
var files = filteredFiles(folder, /\.(coffee|js)$/);
var pending = files.length;
files.forEach(function(file) {
var name = file.substring(0, file.lastIndexOf("."));
timbits.add(name, require(path.join(folder, file)), function() {
pending--;
if (pending === 0) callback();
});
});
}
// automagically load views for a given timbit
function loadViews(timbit) {
timbit.views = [];
var pattern = new RegExp('\.' + timbits.app.settings['view engine'] + '$')
, folder = path.join(config.home, 'views', timbit.viewBase);
if (fs.existsSync(folder)) {
var files = fs.readdirSync(folder);
files.forEach(function(file) {
timbit.views.push(file.replace(pattern, ''));
});
}
// We will attempt the default view anyway and hope the timbit knows what it is doing.
if (timbit.views.length === 0) timbit.views.push(timbit.defaultView);
}
// return a list of possible test values
function getTestValues(values, alltests) {
if (values != null && values.length != null && values.length !== 0) {
if (alltests)
return values;
else
return values.slice(0, 1);
} else {
return [];
}
}
// compile built in templates
function compileTemplate(name) {
var filename = path.join(__dirname, "templates", name + '.hjs');
var contents = fs.readFileSync(filename);
return hogan.compile(contents.toString());
}
// generates the allowed methods
function allowedMethods(methods) {
// default values
var methodsAllowed = { 'GET': true, 'POST': false, 'PUT': false, 'HEAD': false, 'DELETE': false };
// check and override if one of the default methods
for (var key in methods) {
var newKey = key.toUpperCase();
if ( methodsAllowed[newKey]!=undefined) {
methodsAllowed[newKey] = Boolean(methods[key]);
}
}
return methodsAllowed;
}
var timbits = this;
this.box = {};
this.pantry = require('pantry');
this.templates = {
help: compileTemplate('help'),
timbitHelp: compileTemplate('timbit-help'),
test: compileTemplate('test'),
};
this.getLogLevel = function() {
switch (process.env.NODE_ENV) {
case 'production':
return 'info';
case 'test':
return 'error';
default:
return 'silly';
}
};
//added winston object for logging.
this.log = new (winston.Logger)({
transports: [
new (winston.transports.Console)({
colorize: true,
timestamp: true,
level: this.getLogLevel()
})
]
});
// creates, configures, and returns a standard express app
this.serve = function(options) {
/* configure options */
for (var key in options) {
value = options[key];
config[key] = value;
}
/* configure express app */
var app = timbits.app = express();
app.set('views', "" + config.home + "/views");
app.set('view engine', config.engine);
app.set('jsonp callback', config.jsonp);
app.use(express.favicon());
// disable request logging for tests (avoid console clutter)
if (app.get('env') === 'development')
app.use(express.logger('dev'));
app.use(express.compress());
app.use(express.bodyParser());
app.use(express.cookieParser());
app.use(express.static(path.join(config.home, "public")));
app.use(express.static(path.join(__dirname, "../resources")));
app.use(express.errorHandler());
// redirect root to help page
if (config.help) {
app.all(config.base + "/", function(req, res) {
res.redirect(config.base + "/timbits/help");
});
}
// route json discovery
app.get(config.base + "/timbits/json", function(req, res) {
if (config.discovery) {
res.json(timbits.box);
} else {
res.send(404, "Automatic Discovery has been disabled");
}
});
// route help page
app.get(config.base + "/timbits/help", function(req, res) {
if (config.help) {
var context = {title: 'Timbits Help', timbits: []};
for(var key in timbits.box) {
context.timbits.push(key);
}
res.send(timbits.templates.help.render(context));
} else {
res.send(404, "Automatic Help has been disabled");
}
});
// route master test page
app.get(config.base + '/timbits/test/:which?', function(req, res) {
if (config.test) {
var alltests = (req.params.which === 'all')
, all_results = []
, pending = Object.keys(timbits.box).length;
if (pending) {
for (name in timbits.box) {
var timbit = timbits.box[name];
timbit.test('http://' + req.headers.host, alltests, function(results) {
results.forEach(function(result) {
all_results.push(result);
});
if (--pending === 0) {
var passed = 0, failed = 0;
all_results.forEach(function(result) {
if (result.passed) passed++; else failed++;
});
res.send(timbits.templates.test.render({
title: 'Testing Summary: all timbits',
passed: passed,
failed: failed,
results: all_results
}));
}
});
}
} else {
res.send(ck.render(views.test, {}));
}
} else {
res.send(404, "Automatic Test has been disabled");
}
});
// automagically load timbits
loadTimbits(function() {
try {
timbits.server = app.listen(process.env.PORT || process.env.C9_PORT || config.port);
timbits.log.info("Timbits server listening on port " + timbits.server.address().port + " in " + app.settings.env + " mode");
} catch (err) {
timbits.log.error("Server could not start on port " + (process.env.PORT || process.env.C9_PORT || config.port) + ". (" + err + ")");
console.log("\nPress Ctrl+C to Exit");
process.exit(1);
}
});
return app;
};
// use the 'add' method to place a timbit in the box
this.add = function(name, timbit, callback) {
timbits.log.info("Placing " + name + " in the box");
timbits.box[name] = timbit;
timbit.name = name;
if (timbit.viewBase == null) timbit.viewBase = name;
if (timbit.defaultView == null) timbit.defaultView = 'default';
if (timbit.maxAge == null) timbit.maxAge = config.maxAge;
timbit.methods = allowedMethods( (timbit.methods == null) ? {} : timbit.methods );
loadViews(timbit);
// route timbit help
timbits.app.get(config.base + "/" + name + "/help", function(req, res) {
if (config.help)
res.send(timbits.templates.timbitHelp.render(timbit));
else
res.send(404, "Automatic Help has been disabled");
});
// route timbit testing
timbits.app.get(config.base + "/" + name + "/test/:which?", function(req, res) {
var alltests;
if (config.test) {
alltests = req.params.which === 'all';
timbit.test("http://" + req.headers.host, alltests, function(results) {
var passed = 0, failed = 0;
results.forEach(function(result) {
if (result.passed) passed++; else failed++;
});
res.send(timbits.templates.test.render({
title: 'Testing Summary: ' + timbit.name,
passed: passed,
failed: failed,
results: results
}));
});
} else {
res.send(404, "Automatic Test has been disabled");
}
});
// main timbit route
timbits.app.all(config.base + '/' + name + '/:view?', function(req, res) {
// test if the method used is in the allowed methods,
if ( timbit.methods[req.method] == undefined || timbit.methods[req.method] == false ) {
res.send(405, "Method Not Allowed");
return;
}
// set view name to default view if not specified
if (req.params.view == null) req.params.view = timbit.defaultView;
// initialize current request context
var context = {
name: timbit.name,
view: timbit.viewBase + '/' + req.params.view,
maxAge: timbit.maxAge
};
// add query string parameters to context
for (var key in req.query) {
var has_alias = false;
if (context[key] == null && req.query[key] != null && req.query[key] !== '') {
// handle aliased parameters
for (var p in timbit.params) {
if (timbit.params[p] === key) {
has_alias = true;
context[p] = req.query[key];
}
}
// no alias found
if (!has_alias)
context[key] = req.query[key];
}
}
// validate request
for (var key in timbit.params) {
var param = timbit.params[key];
// if parameter isn't specified, use default value
if (context[key] == null) context[key] = param.default;
// test provided value based on type
var value = context[key];
if (value != null) {
// default parameter type is String
if (param.type == null) param.type = 'String';
switch (param.type.toLowerCase()) {
case 'number':
context[key] = Number(value);
if (isNaN(context[key]))
throw value + ' is not a valid Number for ' + key;
break;
case 'boolean':
switch (value.toLowerCase()) {
case 'true':
context[key] = true;
break;
case 'false':
context[key]= false;
break;
default:
throw value + ' is not a valid value for ' + key + '. Must be true of false';
}
break;
case 'date':
context[key] = Date.parse(value);
if (isNaN(context[key]))
throw value + ' is not a valid date for ' + key;
break;
}
}
if (param.required && value == null) {
throw key + " is a required parameter";
}
if (value != null && param.strict && param.values.indexOf(value) === -1) {
throw value + " is not a valid value for " + key + ". Must be one of [" + (param.values.join()) + "]";
}
if (value instanceof Array && !param.multiple) {
throw key + " must be a single value";
}
}
// with context created, it's time to consume this timbit
timbit.eat(req, res, context);
});
// update example urls if base vpath specified
if (config.base != null && timbit.examples != null) {
timbit.examples.forEach(function(example) {
example.href = config.base + example.href;
});
}
// callback after timbit has been loaded
callback();
};
// prototype for Timbit
var Timbit = this.Timbit = function() {};
Timbit.prototype.render = function(req, res, context) {
// add caching headers
res.setHeader("Cache-Control", "max-age=" + context.maxAge);
res.setHeader("Edge-Control", "!no-store, max-age=" + context.maxAge);
if (/^(\w+|(\w+-)+\w+)\/json$/.test(context.view)) {
if (config.json)
res.json(context);
else
res.send(404, "JSON view has been disabled");
} else {
res.render(context.view, context, function(err, str) {
if (err) {
timbits.log.error("Error rendering view " + context.view);
req.next(err);
} else {
if (context.callback != null)
res.json(str);
else
res.send(str);
}
});
}
};
Timbit.prototype.fetch = function(req, res, context, options, callback) {
// use built in render method by default
if (callback == null) callback = this.render;
var name = options.name || 'data';
timbits.pantry.fetch(options, function(error, results) {
if (error) {
timbits.log.error("Error fetching resource '" + options.uri);
req.next(error);
} else {
if (context[name] != null) {
if (Object.prototype.toString.call(context[name][0]) === "[object Array]")
context[name].push(results);
else
context[name] = [context[name], results];
} else {
context[name] = results;
}
callback(req, res, context);
}
});
};
Timbit.prototype.eat = function(req, res, context) {
this.render(req, res, context);
};
Timbit.prototype.generateTests = function(alltests) {
// create combination of required parameters
var required = [];
for (var name in this.params) {
var param = this.params[name];
if (param.required) {
var temp = [];
getTestValues(param.values, alltests).forEach(function(value) {
if (required.length === 0)
temp.push(name + "=" + value);
else
required.forEach(function(item) {
temp.push(item + "&" + name + "=" + value);
});
});
required = temp;
}
}
// create list of possible queries using required and optional parameters
var queries = [];
required.forEach(function(item) {
queries.push(item)
});
// only include optional parameters if all tests are requested
if (alltests) {
for (var name in this.params) {
var param = this.params[name];
if (!param.required) {
getTestValues(param.values, alltests).forEach(function(value) {
if (required.length === 0)
queries.push(name + "=" + value);
else
required.forEach(function(item) {
queries.push(item + "&" + name + "=" + value);
});
});
}
}
}
//create list of testable paths using available views and quiries
var hrefs = [];
var name = this.name
this.views.forEach(function(view) {
if (queries.length)
queries.forEach(function(query) {
hrefs.push("/" + name + "/" + view + "?" + query);
});
else
hrefs.push("/" + name + "/" + view);
});
return hrefs;
};
Timbit.prototype.test = function(host, alltests, callback) {
// generate dynamic list of test urls
var tests = this.generateTests(alltests);
// multiplier for tests total based on allowed methods, determine which methods are used
var testsLength = 1;
var getMethod = this.methods["GET"];
var postMethod = this.methods["POST"];
if (getMethod && postMethod)
testsLength = 2;
// add examples to list of tests
if (this.examples) {
this.examples.forEach(function(example) {
tests.push(example.href);
});
}
// run each test
var results = [];
var name = this.name;
tests.forEach(function(href) {
if (getMethod) {
request(host + href, function(error, response, body) {
error = error || (response.statusCode === 200 ? '' : body);
results.push({
timbit: name,
href: href,
error: error,
status: response.statusCode,
passed: response.statusCode === 200,
failed: response.statusCode !== 200
});
if (results.length === (tests.length * testsLength))
return callback(results);
});
}
if (postMethod) {
request.post(host + href, function(error, response, body) {
error = error || (response.statusCode === 200 ? '' : body);
results.push({
timbit: name,
href: href,
error: error,
status: response.statusCode,
passed: response.statusCode === 200,
failed: response.statusCode !== 200
});
if (results.length === (tests.length * testsLength))
return callback(results);
});
}
});
};
Timbit.prototype.paramsAsArray = function() {
var array = [];
for (var key in this.params) {
array.push({key: key, value: this.params[key]});
}
return array;
}