Created
August 17, 2010 21:55
-
-
Save fnando/532260 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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(); | |
}; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Really interesting! :)