route.js | |
---|---|
var Key = require('./key').Key,
regExpEscape = require('./helpers').regExpEscape,
mixin = require('./helpers').mixin,
kindof = require('./helpers').kindof | |
new Route( path [, method] )turns strings into magical ponies that come when you call them
Pretty familiar to anyone who's used Merb/Rails - called by Router.match() | var Route = function( path, method ) {
var self = this, |
!x! regexen crossing !x! matches keys | KEY = /:([a-zA-Z_][\w\-]*)/, |
optional group (the part in parens) | OGRP = /\(([^)]+)\)/, |
breaks a string into atomic parts: ogrps, keys, then everything else | PARTS = /\([^)]+\)|:[a-zA-Z_][\w\-]*|[\w\-_\\\/\.]+/g |
is this a nested, optional url segment like (.:format) | self.optional = false |
uppercase the method name | if (typeof(method) == 'string') self.method = method.toUpperCase() |
base properties | self.params = {}
self.parts = []
self.route_name = null
self.path = path
/*self.regex = null // for caching of test() regex MAYBE*/ |
route.regexString()returns a composite regex string of all route parts | this.regexString = function() {
var ret = '' |
a route regex is a composite of its parts' regexe(s|n) | for (var i in self.parts) {
var part = self.parts[i]
if (part instanceof Key) {
ret += part.regexString()
} else if (part instanceof Route) {
ret += part.regexString()
} else { // string
ret += regExpEscape(part)
}
}
return '('+ret+')'+(self.optional ? '?' : '')
}; |
route.test( string )builds & tests on a full regex of the entire path
returns true/false depending on whether the url matches | this.test = function( string ) {
/*
TODO cache this if it makes sense, code below:
if(self.regex == null) self.regex = RegExp('^' + self.regexString() + '(\\\?.*)?$')
return self.regex.test(string)
*/
return RegExp('^' + self.regexString() + '(\\\?.*)?$').test(string)
}; |
route.to( endpoint [, extra_params ] )defines the endpoint & mixes in optional params
returns the route for chaining | this.to = function( endpoint, extra_params ) {
if ( !extra_params && typeof endpoint != 'string' ) {
extra_params = endpoint
endpoint = undefined
}
/*
TODO: make endpoint optional, since you can have the
controller & action in the URL utself,
even though that's a terrible idea...
*/
if ( endpoint ){
endpoint = endpoint.split('.')
if( kindof(endpoint) == 'array' && endpoint.length != 2 ) throw 'syntax should be in the form: controller.action'
this.params.controller = endpoint[0]
this.params.action = endpoint[1]
}
extra_params = kindof(extra_params) == 'object' ? extra_params : {}
mixin(self.params, extra_params)
return this // chainable
}; |
route.name( name )just sets the route name - NAMED ROUTES ARE NOT CURRENTLY USED
returns: the route for chaining | this.name = function( name ) {
self.route_name = name
return self // chainable
}; |
route.where( conditions )sets conditions that each url variable must match for the URL to be valid
returns: the route for chaining | this.where = function( conditions ) {
var self = this
if ( kindof(conditions) != 'object' ) throw 'conditions must be an object'
for (var i in self.parts) {
if (self.parts[i] instanceof Key || self.parts[i] instanceof Route) { |
recursively apply all conditions to sub-parts | self.parts[i].where(conditions)
}
}
return self // chainable
};
|
route.stringify( params )builds a string url for this Route from a params object returns: [ "url", [leftover params] ] this is meant to be called & modified by router.url() | this.stringify = function( params ) {
var url = [] // urls start life as an array to enble a second pass
for (var i in self.parts) {
var part = self.parts[i]
if (part instanceof Key) {
if (typeof(params[part.name]) != 'undefined' &&
part.regex.test(params[part.name])) { |
there's a param named this && the param matches the key's regex | url.push(part.url(params[part.name])); // push it onto the stack
delete params[part.name] // and remove from list of params
} else if (self.optional) { |
(sub)route doesn't match, move on | return false
}
} else if (part instanceof Route) { |
sub-routes must be handled in the next pass to avoid leftover param duplication | url.push(part)
} else { // string
url.push(part)
}
}
|
second pass, resolve optional parts | for (var i in url) {
if (url[i] instanceof Route) {
url[i] = url[i].stringify(params) // recursion is your friend |
it resolved to a url fragment! | if (url[i]) { |
replace leftover params hash with the new, smaller leftover params hash | params = url[i][1] |
leave only the string for joining | url[i] = url[i][0]
} else {
delete url[i] // get rid of these shits
}
}
}
for (var i in self.params) { |
remove from leftovers, they're implied in the to() portion of the route | delete params[i]
}
return [ url.join(''), params ]
};
|
route.keysAndRoutes()just the parts that aren't strings. basically returns an array of Key and Route objects | this.keysAndRoutes = function() {
var knr = []
for (var i in self.parts) {
if (self.parts[i] instanceof Key || self.parts[i] instanceof Route) {
knr.push(self.parts[i])
}
}
return knr
}; |
route.keys()just the parts that are Keys returns an array of aforementioned Keys | this.keys = function() {
var keys = []
for (var i in self.parts) {
if (self.parts[i] instanceof Key) {
keys.push(self.parts[i])
}
}
return keys;
}; |
route.parse( url, method )parses a URL into a params object
returns: a params hash || false (if the route doesn't match) this is meant to be called by Router.first() && Router.all() | this.parse = function( urlParam, method ) {
|
parse the URL with the regex & step along with the parts, assigning the vals from the url to the names of the keys as we go (potentially stoopid) | |
let's chop off the QS to make life easier | var url = require('url').parse(urlParam)
var path = url.pathname
var params = {method:method}
for (var key in self.params) { params[key] = self.params[key] } |
if the method doesn't match, gtfo immediately | if (typeof self.method != 'undefined' && self.method != params.method) return false
/* TODO: implement substring checks for possible performance boost */ |
if the route doesn't match the regex, gtfo | if (!self.test(path)) {
return false
} |
parse the URL with the regex | var parts = new RegExp('^' + self.regexString() + '$').exec(path)
var j = 2; // index of the parts array, starts at 2 to bypass the entire match string & the entire match
var keysAndRoutes = self.keysAndRoutes()
for (var i in keysAndRoutes) {
if (keysAndRoutes[i] instanceof Key) {
if (keysAndRoutes[i].test(parts[j])) {
params[keysAndRoutes[i].name] = parts[j]
}
} else if (keysAndRoutes[i] instanceof Route) {
if (keysAndRoutes[i].test(parts[j])) { |
parse the subroute | var subparams = keysAndRoutes[i].parse(parts[j], method)
mixin(params, subparams) |
advance the parts pointer by the number of submatches | j+= parts[j].match(keysAndRoutes[i].regexString()).length-2 || 0
} else {
j++;
}
}
j++;
}
return params
}; |
path parsing | while (part = PARTS.exec(path)) {
self.parts.push(part)
}
|
have to do this in two passes due to RegExp execution limits | for (var i in self.parts) {
if (OGRP.test(self.parts[i])) { // optional group
self.parts[i] = new Route(OGRP.exec(self.parts[i])[1], true)
self.parts[i].optional = true
} else if(KEY.test(self.parts[i])) { // key
var keyname = KEY.exec(self.parts[i])[1]
self.parts[i] = new Key(keyname)
} else { // string
self.parts[i] = String(self.parts[i])
}
}
return self
}; // Route
exports.Route = Route
|