Permalink
Browse files

Added support for url tokens.

  • Loading branch information...
1 parent 76d6867 commit 3478c360c43824180e2a223df7160932c1e0d031 @xavi- committed Feb 13, 2012
Showing with 124 additions and 28 deletions.
  1. +42 −8 README.markdown
  2. +37 −12 index.js
  3. +1 −1 package.json
  4. +44 −7 test/test.js
View
@@ -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
@@ -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
@@ -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.
View
@@ -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)) {
@@ -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) };
}
}
@@ -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;
@@ -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);
}
View
@@ -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":
View
@@ -3,7 +3,7 @@ var fs = require("fs");
var bee = require("../");
var tests = {
- expected: 29,
+ expected: 34,
executed: 0,
finished: function() { tests.executed++; }
};
@@ -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();
@@ -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" });
@@ -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"]);
@@ -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);

0 comments on commit 3478c36

Please sign in to comment.