mach
Version:
HTTP for JavaScript
136 lines (111 loc) • 3.56 kB
JavaScript
;
var d = require("describe-property");
var escapeRegExp = require("../utils/escapeRegExp");
function byMostSpecific(a, b) {
return b.path.length - a.path.length || (b.host || "").length - (a.host || "").length;
}
/**
* A middleware that provides host and/or location-based routing. Modifies
* the `basename` connection variable for all downstream apps such that only
* the portion relevant for dispatch remains in `pathname`.
*
* app.use(mach.mapper, {
*
* 'http://example.com/images': function (conn) {
* // The hostname used in the request was example.com, and
* // the URL path started with "/images". If the request was
* // GET /images/avatar.jpg, then conn.pathname is /avatar.jpg
* },
*
* '/images': function (conn) {
* // The URL path started with "/images"
* }
*
* });
*
* This function may also be used outside of the context of a middleware
* stack to create a standalone app. You can either provide mappings one
* at a time:
*
* var app = mach.mapper();
*
* app.map('/images', function (conn) {
* // ...
* });
*
* Or all at once:
*
* var app = mach.mapper({
*
* '/images': function (conn) {
* // ...
* }
*
* });
*
* Note: Dispatch is done in such a way that the longest paths are tried first
* since they are the most specific.
*/
function createMapper(app, map) {
// Allow mach.mapper(map)
if (typeof app === "object") {
map = app;
app = null;
}
var mappings = [];
function mapper(conn) {
var hostname = conn.hostname;
var pathname = conn.pathname;
var mapping, match, remainingPath;
for (var i = 0, len = mappings.length; i < len; ++i) {
mapping = mappings[i];
// Try to match the hostname.
if (mapping.hostname && mapping.hostname !== hostname) continue;
// Try to match the path.
if (!(match = pathname.match(mapping.pattern))) continue;
// Skip if the remaining path doesn't start with a "/".
remainingPath = match[1];
if (remainingPath.length > 0 && remainingPath[0] !== "/") continue;
conn.basename += mapping.path;
return conn.call(mapping.app);
}
return conn.call(app);
}
Object.defineProperties(mapper, {
/**
* Adds a new mapping that runs the given app when the location used in the
* request matches the given location.
*/
map: d(function (location, app) {
var hostname, path;
// If the location is a fully qualified URL use the host as well.
var match = location.match(/^https?:\/\/(.*?)(\/.*)/);
if (match) {
hostname = match[1].replace(/:\d+$/, ""); // Strip the port.
path = match[2];
} else {
path = location;
}
if (path.charAt(0) !== "/") throw new Error("Mapping path must start with \"/\", was \"" + path + "\"");
path = path.replace(/\/$/, "");
var pattern = new RegExp("^" + escapeRegExp(path).replace(/\/+/g, "/+") + "(.*)");
mappings.push({
hostname: hostname,
path: path,
pattern: pattern,
app: app
});
mappings.sort(byMostSpecific);
}),
/**
* Sets the given app as the default for this mapper.
*/
run: d(function (downstreamApp) {
app = downstreamApp;
})
});
// Allow app.use(mach.mapper, map)
if (typeof map === "object") for (var location in map) if (map.hasOwnProperty(location)) mapper.map(location, map[location]);
return mapper;
}
module.exports = createMapper;