Skip to content

Instantly share code, notes, and snippets.

@fnando
Created August 17, 2010 21:55
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save fnando/532260 to your computer and use it in GitHub Desktop.
Save fnando/532260 to your computer and use it in GitHub Desktop.
// Load dependencies
var app = require("./lib/server").createServer(),
sys = require("sys");
// Start app server
app.listen(3002);
app.get("/:name", function(req, res){
res.render("Hello " + req.params.name);
});
app.get(/\/([^\/]+)/, function(req, res){
res.renderJson(req.params);
});
app.get("/", function(req, res){
res.render("Hello world!");
});
app.post("/", function(req, res){
res.renderJson(req.params);
});
app.get("/archives", "index.html"); // render index.html
// Add vendor directory to load path
require.paths.unshift(__dirname + "/../vendor");
// Load dependencies
var http = require("http"),
url = require("url"),
sys = require("sys"),
fs = require("fs"),
querystring = require("querystring"),
DEBUG = false;
var log = function(message) {
if (DEBUG) {
sys.log(message);
}
};
var inspect = function(message, object) {
if (DEBUG) {
sys.log(message + " => " + sys.inspect(object));
}
};
// Create a new HTTP server. When a request comes in a recognized
// route, the handler will invoked and can expect two arguments: request and response,
// in that order. To reply something to this request you have to call
// response.render or response.renderJson.
//
// If no route is found, then the 404 status is returned.
//
// The response.render function expects a string body and object of headers.
//
// response.render("Hello world!", {"Content-Type": "text/plain"});
//
// The response.renderJson works in similar manner but expects an object that can be
// serialized as JSON string.
//
// response.renderJson({message: "Hello world!"})
//
// You can also render static files that lives on the `publicDir` directory.
//
// response.renderStatic("index.html")
//
function Server() {
// Hold instance for later use
var self = this;
// Hold parsed ARGV.
this.argv = this.parseARGV();
// Hold static files
this.publicDir = "./public";
// Hold all recognized routes
this.routes = {
GET: []
, POST: []
, PUT: []
, DELETE: []
, HEAD: []
, OPTIONS: []
};
// Set current environment.
this.environment = this.argv.environment || "development";
// Set debugging.
this.debug = false;
// Hold configuration for multiple environments.
this.configurations = {};
// Cache static files.
this.cached = {};
// Set if static files should be cached in memory.
this.cache = true;
// Hold recognized HTTP status code.
// This list is used on the `renderStatus()` function.
this.httpStatuses = {
200: "OK"
, 201: "Created"
, 202: "Accepted"
, 203: "Non-Authoritative Information"
, 204: "No Content"
, 205: "Reset Content"
, 206: "Partial Content"
, 300: "Multiple Choices"
, 301: "Moved Permanently"
, 302: "Found"
, 303: "See Other"
, 304: "Not Modified"
, 305: "Use Proxy"
, 307: "Temporary Redirect"
, 400: "Bad Request"
, 401: "Unauthorized"
, 402: "Payment Required"
, 403: "Forbidden"
, 404: "Not Found"
, 405: "Method Not Allowed"
, 406: "Not Acceptable"
, 407: "Proxy Authentication Required"
, 408: "Request Timeout"
, 409: "Conflict"
, 410: "Gone"
, 411: "Length Required"
, 412: "Precondition Failed"
, 413: "Request Entity Too Large"
, 414: "Request-URI Too Long"
, 415: "Unsupported Media Type"
, 416: "Requested Range Not Satisfiable"
, 417: "Expectation Failed"
, 500: "Internal Server Error"
, 501: "Not Implemented"
, 502: "Bad Gateway"
, 503: "Service Unavailable"
, 504: "Gateway Timeout"
, 505: "HTTP Version Not Supported"
};
// Hold recognized content types
this.mimeTypes = {
"3gp" : "video/3gpp"
, "a" : "application/octet-stream"
, "ai" : "application/postscript"
, "aif" : "audio/x-aiff"
, "aiff" : "audio/x-aiff"
, "asc" : "application/pgp-signature"
, "asf" : "video/x-ms-asf"
, "asm" : "text/x-asm"
, "asx" : "video/x-ms-asf"
, "atom" : "application/atom+xml"
, "au" : "audio/basic"
, "avi" : "video/x-msvideo"
, "bat" : "application/x-msdownload"
, "bin" : "application/octet-stream"
, "bmp" : "image/bmp"
, "bz2" : "application/x-bzip2"
, "c" : "text/x-c"
, "cab" : "application/vnd.ms-cab-compressed"
, "cc" : "text/x-c"
, "chm" : "application/vnd.ms-htmlhelp"
, "class" : "application/octet-stream"
, "com" : "application/x-msdownload"
, "conf" : "text/plain"
, "cpp" : "text/x-c"
, "crt" : "application/x-x509-ca-cert"
, "css" : "text/css"
, "csv" : "text/csv"
, "cxx" : "text/x-c"
, "deb" : "application/x-debian-package"
, "der" : "application/x-x509-ca-cert"
, "diff" : "text/x-diff"
, "djv" : "image/vnd.djvu"
, "djvu" : "image/vnd.djvu"
, "dll" : "application/x-msdownload"
, "dmg" : "application/octet-stream"
, "doc" : "application/msword"
, "dot" : "application/msword"
, "dtd" : "application/xml-dtd"
, "dvi" : "application/x-dvi"
, "ear" : "application/java-archive"
, "eml" : "message/rfc822"
, "eot" : "application/vnd.ms-fontobject"
, "eps" : "application/postscript"
, "exe" : "application/x-msdownload"
, "f" : "text/x-fortran"
, "f77" : "text/x-fortran"
, "f90" : "text/x-fortran"
, "flv" : "video/x-flv"
, "for" : "text/x-fortran"
, "gem" : "application/octet-stream"
, "gemspec": "text/x-script.ruby"
, "gif" : "image/gif"
, "gz" : "application/x-gzip"
, "h" : "text/x-c"
, "hh" : "text/x-c"
, "htm" : "text/html"
, "html" : "text/html"
, "ico" : "image/vnd.microsoft.icon"
, "ics" : "text/calendar"
, "ifb" : "text/calendar"
, "iso" : "application/octet-stream"
, "jar" : "application/java-archive"
, "java" : "text/x-java-source"
, "jnlp" : "application/x-java-jnlp-file"
, "jpeg" : "image/jpeg"
, "jpg" : "image/jpeg"
, "js" : "application/javascript"
, "json" : "application/json"
, "log" : "text/plain"
, "m3u" : "audio/x-mpegurl"
, "m4v" : "video/mp4"
, "man" : "text/troff"
, "mathml" : "application/mathml+xml"
, "mbox" : "application/mbox"
, "mdoc" : "text/troff"
, "me" : "text/troff"
, "mid" : "audio/midi"
, "midi" : "audio/midi"
, "mime" : "message/rfc822"
, "mml" : "application/mathml+xml"
, "mng" : "video/x-mng"
, "mov" : "video/quicktime"
, "mp3" : "audio/mpeg"
, "mp4" : "video/mp4"
, "mp4v" : "video/mp4"
, "mpeg" : "video/mpeg"
, "mpg" : "video/mpeg"
, "ms" : "text/troff"
, "msi" : "application/x-msdownload"
, "odp" : "application/vnd.oasis.opendocument.presentation"
, "ods" : "application/vnd.oasis.opendocument.spreadsheet"
, "odt" : "application/vnd.oasis.opendocument.text"
, "ogg" : "application/ogg"
, "otf" : "font/otf"
, "p" : "text/x-pascal"
, "pas" : "text/x-pascal"
, "pbm" : "image/x-portable-bitmap"
, "pdf" : "application/pdf"
, "pem" : "application/x-x509-ca-cert"
, "pgm" : "image/x-portable-graymap"
, "pgp" : "application/pgp-encrypted"
, "pkg" : "application/octet-stream"
, "pl" : "text/x-script.perl"
, "pm" : "text/x-script.perl-module"
, "png" : "image/png"
, "pnm" : "image/x-portable-anymap"
, "ppm" : "image/x-portable-pixmap"
, "pps" : "application/vnd.ms-powerpoint"
, "ppt" : "application/vnd.ms-powerpoint"
, "ps" : "application/postscript"
, "psd" : "image/vnd.adobe.photoshop"
, "py" : "text/x-script.python"
, "qt" : "video/quicktime"
, "ra" : "audio/x-pn-realaudio"
, "rake" : "text/x-script.ruby"
, "ram" : "audio/x-pn-realaudio"
, "rar" : "application/x-rar-compressed"
, "rb" : "text/x-script.ruby"
, "rdf" : "application/rdf+xml"
, "roff" : "text/troff"
, "rpm" : "application/x-redhat-package-manager"
, "rss" : "application/rss+xml"
, "rtf" : "application/rtf"
, "ru" : "text/x-script.ruby"
, "s" : "text/x-asm"
, "sgm" : "text/sgml"
, "sgml" : "text/sgml"
, "sh" : "application/x-sh"
, "sig" : "application/pgp-signature"
, "snd" : "audio/basic"
, "so" : "application/octet-stream"
, "svg" : "image/svg+xml"
, "svgz" : "image/svg+xml"
, "swf" : "application/x-shockwave-flash"
, "t" : "text/troff"
, "tar" : "application/x-tar"
, "tbz" : "application/x-bzip-compressed-tar"
, "tcl" : "application/x-tcl"
, "tex" : "application/x-tex"
, "texi" : "application/x-texinfo"
, "texinfo": "application/x-texinfo"
, "text" : "text/plain"
, "tif" : "image/tiff"
, "tiff" : "image/tiff"
, "torrent": "application/x-bittorrent"
, "tr" : "text/troff"
, "ttf" : "font/ttf"
, "txt" : "text/plain"
, "vcf" : "text/x-vcard"
, "vcs" : "text/x-vcalendar"
, "vrml" : "model/vrml"
, "war" : "application/java-archive"
, "wav" : "audio/x-wav"
, "wma" : "audio/x-ms-wma"
, "wmv" : "video/x-ms-wmv"
, "wmx" : "video/x-ms-wmx"
, "wrl" : "model/vrml"
, "wsdl" : "application/wsdl+xml"
, "xbm" : "image/x-xbitmap"
, "xhtml" : "application/xhtml+xml"
, "xls" : "application/vnd.ms-excel"
, "xml" : "application/xml"
, "xpm" : "image/x-xpixmap"
, "xsl" : "application/xml"
, "xslt" : "application/xslt+xml"
, "yaml" : "text/yaml"
, "yml" : "text/yaml"
, "zip" : "application/zip"
};
// Get HTTP server instance.
this.http = http.createServer();
this.http.addListener("request", function(request, response){
self.onRequest(self, request, response);
});
};
// Run configuration for the specified environment.
Server.prototype.runConfiguration = function(environment) {
var config = this.configurations[environment];
if (!config) {
return;
};
for (var i in config) {
config[i].call(this);
};
};
// Handle incoming requests.
// The response object is extended with some render functions
Server.prototype.onRequest = function(self, request, response){
log("Retrieving route...");
var route = self.routeFor(request);
var data = "";
DEBUG = self.debug;
inspect("route", route);
log(request.method + " on " + url.parse(request.url).href);
// Render a HTTP status code.
//
// response.renderStatus(404);
//
response.renderStatus = function(status) {
var body = status + " " + self.httpStatuses[status];
this.writeHead(status, {"Content-Type": "text/plain", "Content-Length": body.length});
this.end(body);
};
// Render a text response. You can provide `headers` with
// any additional header you want to send, including `Content-Type`,
// which defaults to `text/plain`.
// If no `status` is provided, 200 is returned.
//
// response.render("Hi there!");
// response.render("<p>Hi there!</p>", {"Content-Type": "text/html"});
// response.render("<p>NOT FOUND!</p>", {"Content-Type": "text/html", status: 404});
//
response.render = function(body, headers) {
if (!headers) {
headers = {"Content-Type": "text/plain"};
}
var status = headers.status || 200;
delete(headers.status);
headers["Content-Length"] = body.length;
this.writeHead(status, headers);
this.end(body);
};
// Render `body` as its JSON representation.
// You can provide `headers` with any additional header you want to send.
//
// response.renderJson({name: "John Doe"});
//
response.renderJson = function(body, headers) {
if (!headers) {
headers = {};
}
headers["Content-Type"] = "application/json";
this.render(JSON.stringify(body), headers);
};
// Render static files that lives on `publicDir`.
// If no file is found, status 404 is returned.
//
// response.renderStatic("index.html");
//
response.renderStatic = function(file) {
var fullPath = self.publicDir + "/" + file.replace(/^\//, "");
var handler = function(body) {
var mime = "application/octet-stream",
parts = fullPath.split("."),
extension = parts[parts.length - 1];
if (extension) {
mime = self.mimeTypes[extension.toLowerCase()] || mime;
}
return response.render(body, {"Content-Type": mime});
};
// If there's a cached file, don't read it again.
if (self.cached[file] && self.cache) {
log("static file is cached: " + file);
return handler(self.cached[file]);
};
// Well, file is not cached yet, so let's read it from filesystem.
fs.stat(fullPath, function(error, stats){
if (error) {
inspect("couldn't find static file", fullPath);
return response.renderStatus(404);
}
fs.readFile(fullPath, function(error, body){
if (error) {
inspect("error while reading static file", error);
return response.renderStatus(500);
}
self.cached[file] = body;
return handler(body);
});
});
};
if (!route) {
return response.renderStatic(url.parse(request.url).pathname);
};
if (typeof(route.handler) == "string") {
return response.renderStatic(route.handler);
};
request.addListener("data", function(chunk){
data += chunk;
});
request.addListener("end", function(){
request.body = data;
request.params = Object.merge(
querystring.parse(url.parse(request.url).query),
querystring.parse(data),
route.params
);
inspect("params", request.params);
inspect("headers", request.headers);
route.handler.call(self, request, response);
});
};
// Set a configuration that will be executed when server starts running
// on the specified environment.
//
// app.configure("development", function(){
// this.cache = false;
// });
//
// Remember to set configuration before calling `app.listen()`; otherwise,
// you configuration won't be ran.
//
Server.prototype.configure = function() {
var environment, handler;
if (arguments.length == 1) {
environment = "global";
handler = arguments[0];
} else {
environment = arguments[0];
handler = arguments[1];
};
if (!this.configurations[environment]) {
this.configurations[environment] = [];
};
this.configurations[environment].push(handler);
};
// Register a new route.
Server.prototype.registerRoute = function(method, pattern, handler) {
this.routes[method].push({pattern: pattern, handler: handler});
};
// Parse ARGV parameters.
// The accepted formats are `--name=value` and `--name="some string"`
//
Server.prototype.parseARGV = function() {
var argv = {},
arg, matches;
for (var i in process.argv) {
arg = process.argv[i];
matches = arg.match(/--([a-z0-9_-]+)(?:=(?:["']?([^'"]+)["']?|[^\s]+))/i);
if (!matches) {
continue;
};
argv[matches[1]] = matches[2] || true;
};
return argv;
};
// Detect route. You can register a string with placeholders or a regular expression.
//
// app.get("/articles/:page", callback);
// app.get(/\/articles\/(\d+)/, callback);
// app.get("/", "index.html");
//
// Matched arguments will be stored on the `request.params` object.
Server.prototype.routeFor = function(request) {
var uri = url.parse(request.url),
routes = this.routes[request.method],
matches, i, route;
inspect("routes", routes);
for (i = 0; route = routes[i]; i++) {
if (typeof(route.pattern) == "string") {
var names = route.pattern.match(/(:[a-z0-9_]+)/gi);
var compiled = RegExp.escape(route.pattern).replace(/(:[a-z0-9_]+)/gim, "([^\\/]+)");
var re = new RegExp("^" + compiled + "$");
matches = uri.pathname.match(re);
inspect("route regex", re);
if (!matches) {
continue;
}
var result = {
handler: route.handler,
params: {}
};
if (names) {
for (var j = 0, name; name = names[j]; j++) {
result.params[name.replace(/:/, "")] = querystring.unescape(matches[j + 1]);
}
}
return result;
} else {
matches = uri.pathname.match(route.pattern);
var splat = [];
if (!matches) {
return;
}
matches = matches.splice(1);
for (i in matches) {
splat.push(querystring.unescape(matches[i]));
}
return {
handler: route.handler,
params: {splat: splat}
};
}
};
};
// Escape special characters from strings that must be
// interpreted as regular expression.
RegExp.escape = function(text) {
return text.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&");
};
// Mix several objects together.
//
// var data = {name: "John Doe"};
// data.merge({email: "john@example.com"});
//
// The above example will return `{name: "John Doe", email: "john@example.com"}`.
//
Object.merge = function() {
var merger = function(target, source) {
for (var i in source) {
target[i] = source[i];
}
return target;
};
var result = {};
for (var i = 0; i < arguments.length; i++) {
result = merger(result, arguments[i]);
}
return result;
};
// Register a new GET route.
//
// app.get(function(req, res){
// res.renderJson({id: 1});
// })
//
Server.prototype.get = function(route, handler) {
this.registerRoute("GET", route, handler);
};
// Register a new POST route.
//
// app.post(function(req, res){
// res.renderJson({id: 1});
// })
//
Server.prototype.post = function(route, handler) {
this.registerRoute("POST", route, handler);
};
// Register a new PUT route.
//
// app.put(function(req, res){
// res.renderJson({id: 1});
// })
//
Server.prototype.put = function(route, handler) {
this.registerRoute("PUT", route, handler);
};
// Register a new DELETE route.
//
// app.delete(function(req, res){
// res.renderJson({id: 1});
// })
//
Server.prototype.delete = function(route, handler) {
this.registerRoute("DELETE", route, handler);
};
// Register a new HEAD route.
//
// app.head(function(req, res){
// res.renderJson({id: 1});
// })
//
Server.prototype.head = function(route, handler) {
this.registerRoute("HEAD", route, handler);
};
// Register a new OPTIONS route.
//
// app.options(function(req, res){
// res.renderJson({id: 1});
// })
//
Server.prototype.options = function(route, handler) {
this.registerRoute("OPTIONS", route, handler);
};
// Listen to the host:port address checking for recognized routes.
//
// app.listen(3000)
// app.listen(3000, "localhost")
//
Server.prototype.listen = function(port, host) {
DEBUG = this.debug;
this.http.listen(port, host);
sys.puts("Server running at http://" + (host || "127.0.0.1") + ":" + port + "/");
// Run configuration functions
this.runConfiguration("global");
this.runConfiguration(this.environment);
};
// Wrap setInterval so we can propagate server instance as `this`.
// The interval timeout defaults to `1000`.
//
// server.async(function(){ /* do something */ });
// server.async(function(){ /* do something */ }, 5000);
//
Server.prototype.async = function(callback, timeout) {
if (!timeout) {
timeout = 1000;
}
return setInterval(function(self){
callback.call(self);
}, timeout, this);
};
// Export interface
module.exports = Server;
module.exports.log = log;
module.exports.inspect = inspect;
module.exports.createServer = function() {
return new Server();
};
@leandrosilva
Copy link

Really interesting! :)

@gleicon
Copy link

gleicon commented Aug 22, 2010

nando.js

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment