Skip to content

Commit

Permalink
Added support for url tokens.
Browse files Browse the repository at this point in the history
  • Loading branch information
xavi- committed Feb 13, 2012
1 parent 76d6867 commit 3478c36
Show file tree
Hide file tree
Showing 4 changed files with 124 additions and 28 deletions.
50 changes: 42 additions & 8 deletions README.markdown
Expand Up @@ -18,10 +18,33 @@ Currently works with node.js v0.3.1 and above
"/cheggit": function(req, res) {
// Called when req.url === "/cheggit" or req.url === "/cheggit?woo=poo"
},
"r`^/name/([\\w]+)/([\\w]+)$`": function(req, res, matches) {
// Called when req.url matches this regex: "^/name/([\\w]+)/([\\w]+)$"
"/names/`last-name`/`first-name`": function(req, res, tokens, values) {
// Called when req.url contains three parts, the first of is "name".
// The parameter tokens is an object that maps token names to values.
// For example if req.url === "/names/smith/will"
// then tokens === { "first-name": "will", "last-name": "smith" }
// and values === [ "will", "smith" ]
},
"/static/`path...`": function(req, res, tokens, values) {
// Called when req.url starts with "/static/"
// The parameter tokens is an object that maps token name to a value
// The parameter values is a list of
// For example if req.url === "/static/pictures/actors/smith/will.jpg"
// then tokens === { "path": "pictures/actors/smith/will.jpg" }
// and values === [ "pictures/actors/smith/will.jpg" ]
},
"/`user`/static/`path...`": function(req, res, tokens, values) {
// Called when req.url contains at least three parts, the second of which is "static"
// The parameter tokens is an object that maps token names and value
// For example if req.url === "/da-oozer/static/pictures/venkman.jpg"
// then tokens === { "user": "da-oozer", "path": "pictures/venkman.jpg" }
// and values === [ "da-oozer", pictures/venkman.jpg" ]
},
"r`^/actors/([\\w]+)/([\\w]+)$`": function(req, res, matches) {
// Called when req.url matches this regex: "^/actors/([\\w]+)/([\\w]+)$"
// An array of captured groups is passed as the third parameter
// For example if req.url === "/name/smith/will" then matches === [ "smith", "will" ]
// For example if req.url === "/actors/smith/will"
// then matches === [ "smith", "will" ]
},
"`404`": function(req, res) {
// Called when no other route rule are matched
Expand Down Expand Up @@ -81,13 +104,13 @@ To start, simply store the `beeline` library in a local variable:

The `beeline` library contains the following three methods:

- `bee.route(routes)`: Used to create a new router. It returns a function called `rtn_fn` that takes [ServerRequest](http://nodejs.org/docs/v0.4.5/api/http.html#http.ServerRequest) and [ServerResponse](http://nodejs.org/docs/v0.4.5/api/http.html#http.ServerResponse) objects as parameters. The `routes` parameter is an objects that maps rules to handlers. See examples section for more details.
- `bee.staticFile(path, mimeType)`: This is a utility method that is used to quickly expose static files. It returns a function called `rtn_fn` that takes [ServerRequest](http://nodejs.org/docs/v0.4.5/api/http.html#http.ServerRequest) and [ServerResponse](http://nodejs.org/docs/v0.4.5/api/http.html#http.ServerResponse) objects as parameters. When `rtn_fn` is called, the file contents located at `path` are served (via the ServerResponse) with the `Content-Type` set to the `mimeType` parameter. If the file at `path` does not exist a `404` is served. Note that the `Cache-Control` header on the response is given a very large max age and all `Set-Cookie` headers are removed. Here's an example of how you might use `bee.staticFile`:
- `bee.route(routes)`: Used to create a new router. It returns a function called `rtn_fn` that takes [ServerRequest](http://nodejs.org/docs/v0.6.10/api/http.html#http.ServerRequest) and [ServerResponse](http://nodejs.org/docs/v0.6.10/api/http.html#http.ServerResponse) objects as parameters. The `routes` parameter is an objects that maps rules to handlers. See examples section for more details.
- `bee.staticFile(path, mimeType)`: This is a utility method that is used to quickly expose static files. It returns a function called `rtn_fn` that takes [ServerRequest](http://nodejs.org/docs/v0.6.10/api/http.html#http.ServerRequest) and [ServerResponse](http://nodejs.org/docs/v0.6.10/api/http.html#http.ServerResponse) objects as parameters. When `rtn_fn` is called, the file contents located at `path` are served (via the ServerResponse) with the `Content-Type` set to the `mimeType` parameter. If the file at `path` does not exist a `404` is served. Note that the `Cache-Control` header on the response is given a very large max age and all `Set-Cookie` headers are removed. Here's an example of how you might use `bee.staticFile`:

bee.route({
"/robots.txt": bee.staticFile("./content/robots.txt", "text/plain")
});
- `bee.staticDir(path, mimeTypes)`: This is utility method is used to expose directories of files. It returns a function called `rtn_fn` that takes a [ServerRequest](http://nodejs.org/docs/v0.4.5/api/http.html#http.ServerRequest) object, a [ServerResponse](http://nodejs.org/docs/v0.4.5/api/http.html#http.ServerResponse) object, and an array of strings called `matches` as parameters. Whenever `rtn_fn` is called, the items of `matches` are joined together and then concatenated to `path`. The resulting string is assumed to be a path to a specific file. If this file exists, its contents are served (via the ServerResponse) with the `Content-Type` set to the value that corresponds to the file's extension in the `mimeTypes` object. If the resulting string doesn't point to an existing file or if the file's extension is not found in `mimeTypes`, then a `404` is served. Also, file extensions require a leading period (`.`) and are assumed to be lowercase. Note that the `Cache-Control` header on the response is given a very large max age and all `Set-Cookie` headers are removed. Here's an example of how you might use `bee.staticDir`:
- `bee.staticDir(path, mimeTypes)`: This is utility method is used to expose directories of files. It returns a function called `rtn_fn` that takes a [ServerRequest](http://nodejs.org/docs/v0.6.10/api/http.html#http.ServerRequest) object, a [ServerResponse](http://nodejs.org/docs/v0.6.10/api/http.html#http.ServerResponse) object, an optional third parameter, and an array of strings called `matches` as parameters. Whenever `rtn_fn` is called, the items of `matches` are joined together and then concatenated to `path`. The resulting string is assumed to be a path to a specific file. If this file exists, its contents are served (via the ServerResponse) with the `Content-Type` set to the value that corresponds to the file's extension in the `mimeTypes` object. If the resulting string doesn't point to an existing file or if the file's extension is not found in `mimeTypes`, then a `404` is served. Also, file extensions require a leading period (`.`) and are assumed to be lowercase. Note that the `Cache-Control` header on the response is given a very large max age and all `Set-Cookie` headers are removed. Here's an example of how you might use `bee.staticDir`:

bee.route({
// /pics/mofo.png serves ./content/pics/mofo.png
Expand All @@ -96,15 +119,26 @@ The `beeline` library contains the following three methods:
// This helps prevent accidental exposure.
"r`^/pics/(.*)$`":
bee.staticDir("./content/pics/", { ".gif": "image/gif", ".png": "image/png",
".jpg": "image/jpeg", ".jpeg": "image/jpeg" })
".jpg": "image/jpeg", ".jpeg": "image/jpeg" }),
// Also works with URLs with tokens
// /static/help/faq.html serves ./static/help/faq.html
// /static/properties.json serves a 404 since there's no corresponding mimeType specified.
"/static/`path...`":
bee.staticDir("./static/", { ".txt": "text/plain", ".html": "text/html",
".css": "text/css", ".xml": "text/xml" }),
// More complicated path constructs also works
// /will-smith/img-library/headshots/sexy42.jpg serves ./user-images/will-smith/headshots/sexy42.jpg
"/`user`/img-library/`path...`":
bee.staticDir("./user-images/", { ".jpg": "image/jpeg", ".jpeg": "image/jpeg" })
});

### Precedence Rules

In the event that a request matches two rules, the following precedence rules are considered:

- Fully defined rules take highest precedence. In other words, `"/index"` has a higher precedences then ``"r`^/index$`"`` even though semantically both rules are exactly the same.
- Regex rules take higher precedence than `404`
- Tokens and RegExp rules have the same precednce
- RegExp rules take higher precedence than `404`
- `404` has the lowest precedences
- The `503` rules is outside the precedence rules. It can potentially be triggered at any time.

Expand Down
49 changes: 37 additions & 12 deletions index.js
Expand Up @@ -71,8 +71,9 @@
}
}

return function(req, res, match) {
var filePath = path.join.apply(path, [ fileDir ].concat(match));
return function(req, res, extra, matches) {
matches = matches || extra;
var filePath = path.join.apply(path, [ fileDir ].concat(matches));
var ext = path.extname(filePath).toLowerCase();

if(!(ext in mimeLookup)) {
Expand Down Expand Up @@ -110,8 +111,8 @@

function findPattern(patterns, path) {
for(var i = 0, l = patterns.length; i < l; i++) {
if(patterns[i].regx.test(path)) {
return { handler: patterns[i].handler, extra: patterns[i].regx.exec(path).slice(1) };
if(patterns[i].regex.test(path)) {
return { handler: patterns[i].handler, extra: patterns[i].regex.exec(path).slice(1) };
}
}

Expand All @@ -125,8 +126,26 @@

return null;
}

var rPattern = /^r`(.*)`$/;

var rRegExUrl = /^r`(.*)`$/, rToken = /`(.*?)(\.\.\.)?`/g;
function createTokenHandler(names, handler) {
return function(req, res, vals) {
var extra = Object.create(null);
for(var i = 0; i < names.length; i++) {
extra[names[i]] = vals[i];
}
handler.call(this, req, res, extra, vals);
};
}
function parseToken(rule, handler) {
var tokens = [];
var transform = rule.replace(rToken, function replaceToken(_, token, isExtend) {
tokens.push(token);
return (isExtend ? "(.*?)" : "([^/]*?)");
});
var rRule = new RegExp("^" + transform + "$");
return { regex: rRule, handler: createTokenHandler(tokens, handler) };
}
function route(routes) {
var preprocess = [], urls = {}, patterns = [], generics = [], missing = default404, error = default503;

Expand Down Expand Up @@ -162,14 +181,20 @@
} else if(rule === "`503`" || rule === "`error`") {
if(error !== default503) { console.warn("Duplicate beeline rule: " + rule); }
error = routes[key];
} else if(rPattern.test(rule)) {
var rRule = new RegExp(rPattern.exec(rule)[1]);
if(patterns.some(function(p) { return p.regx.toString() === rRule.toString(); })) {
console.warn("Duplicate beeline rule: " + rule);
}
patterns.push({ regx: rRule, handler: routes[key] });
} else if(rule === "`generics`") {
Array.prototype.push.apply(generics, routes[key]);
} else if(rRegExUrl.test(rule)) {
var rRule = new RegExp(rRegExUrl.exec(rule)[1]);
if(patterns.some(function(p) { return p.regex.toString() === rRule.toString(); })) {
console.warn("Duplicate beeline rule: " + rule);
}
patterns.push({ regex: rRule, handler: routes[key] });
} else if(rToken.test(rule)) {
var pattern = parseToken(rule, routes[key]);
if(patterns.some(function(p) { return p.regex.toString() === pattern.regex.toString(); })) {
console.warn("Duplicate beeline rule: " + rule);
}
patterns.push(pattern);
} else {
console.warn("Invalid beeline rule: " + rule);
}
Expand Down
2 changes: 1 addition & 1 deletion package.json
@@ -1,5 +1,5 @@
{ "name": "beeline"
, "version": "0.1.8"
, "version": "0.1.9"
, "description": "A laughably simplistic router for node.js"
, "keywords": [ "url", "dispatch", "router", "request handler", "middleware" ]
, "maintainers":
Expand Down
51 changes: 44 additions & 7 deletions test/test.js
Expand Up @@ -3,7 +3,7 @@ var fs = require("fs");
var bee = require("../");

var tests = {
expected: 29,
expected: 34,
executed: 0,
finished: function() { tests.executed++; }
};
Expand All @@ -13,8 +13,25 @@ console.warn = function(msg) { warnings[msg] = true; tests.finished(); };
var router = bee.route({
"/test": function(req, res) { assert.equal(req.url, "/test?param=1&woo=2"); tests.finished(); },
"/throw-error": function(req, res) { throw Error("503 should catch"); },
"r`^/name/([\\w]+)/([\\w]+)$`": function(req, res, matches) {
assert.equal(req.url, "/name/smith/will");
"/names/`last-name`/`first-name`": function(req, res, tokens) {
assert.equal(req.url, "/names/smith/will");
assert.equal(tokens["first-name"], "will");
assert.equal(tokens["last-name"], "smith");
tests.finished();
},
"/static/`path...`": function(req, res, tokens) {
assert.equal(req.url, "/static/pictures/actors/smith/will.jpg");
assert.equal(tokens["path"], "pictures/actors/smith/will.jpg");
tests.finished();
},
"/`user`/static/`path...`": function(req, res, tokens) {
assert.equal(req.url, "/da-oozer/static/pictures/venkman.jpg");
assert.equal(tokens["user"], "da-oozer");
assert.equal(tokens["path"], "pictures/venkman.jpg");
tests.finished();
},
"r`^/actors/([\\w]+)/([\\w]+)$`": function(req, res, matches) {
assert.equal(req.url, "/actors/smith/will");
assert.equal(matches[0], "smith");
assert.equal(matches[1], "will");
tests.finished();
Expand Down Expand Up @@ -42,7 +59,10 @@ var router = bee.route({
});
router({ url: "/test?param=1&woo=2" });
router({ url: "/throw-error" });
router({ url: "/name/smith/will" });
router({ url: "/names/smith/will" });
router({ url: "/actors/smith/will" });
router({ url: "/da-oozer/static/pictures/venkman.jpg" });
router({ url: "/static/pictures/actors/smith/will.jpg" });
router({ url: "/random", triggerGeneric: true });
router({ url: "/url-not-found" });

Expand Down Expand Up @@ -82,14 +102,16 @@ router({ url: "/test-preprocess" }, {});
// Testing warning messages
router.add({
"/home": function() { },
"r`^/name/([\\w]+)/([\\w]+)$`": function() { },
"r`^/actors/([\\w]+)/([\\w]+)$`": function() { },
"/`user`/static/`path...`": function() { },
"`404`": function() { },
"`503`": function() { },
"`not-a-valid-rule": function() { }
});

assert.ok(warnings["Duplicate beeline rule: /home"]);
assert.ok(warnings["Duplicate beeline rule: r`^/name/([\\w]+)/([\\w]+)$`"]);
assert.ok(warnings["Duplicate beeline rule: r`^/actors/([\\w]+)/([\\w]+)$`"]);
assert.ok(warnings["Duplicate beeline rule: /`user`/static/`path...`"]);
assert.ok(warnings["Duplicate beeline rule: `404`"]);
assert.ok(warnings["Duplicate beeline rule: `503`"]);
assert.ok(warnings["Invalid beeline rule: `not-a-valid-rule"]);
Expand Down Expand Up @@ -181,11 +203,26 @@ fs.readFile("../package.json", function(err, data) {
},
end: function(body) {
assert.deepEqual(body, data);
fs.unwatchFile("../package.json");
fs.unwatchFile("../package.json"); // Internally beelines watches files for changes
tests.finished();
}
}, [ "package.json" ]);
});
fs.readFile("../package.json", function(err, data) {
if(err) { throw err; }

var isHeadWritten = false, setHeaders = {};
staticDir({ headers: {}, url: "/test" }, { // Mock response
setHeader: function(type, val) { },
writeHead: function(status, headers) { },
removeHeader: function(header) { },
end: function(body) {
assert.deepEqual(body, data);
fs.unwatchFile("../package.json"); // Internally beelines watches files for changes
tests.finished();
}
}, { optional: "third parameter" }, [ "package.json" ]);
});
staticDir({ url: "/test" }, { // Mock response
writeHead: function(status, headers) {
assert.equal(status, 404);
Expand Down

0 comments on commit 3478c36

Please sign in to comment.