{ Key, Glob } = require './key'
{ Text } = require './text'
{ Resource } = require './resource'
inflection = require 'inflection'
{ Key, Glob } = require './key'
{ Text } = require './text'
{ Resource } = require './resource'
inflection = require 'inflection'
!x! regexen crossing !x! matches keys
KEY = /:([a-zA-Z_][\w\-]*)/
matches globs
GLOB = /\*([a-zA-Z_][\w\-\/]*)/
optional group (the part in parens)
OGRP = /\(([^)]+)\)/
PARTS = ///
\([^)]+\) # OGRPS
| :[a-zA-Z_][\w\-]* # KEYS
| \*[a-zA-Z_][\w\-]* # GLOBS
| [\w\-_\\\/\.]+ # TEXT
///g
turns strings into magical ponies that come when you call them
route = new Route(router, '/:controller/:action/:id(.:format)')
route = new Route(router, '/:controller/:action(/:id)(.:format)', 'GET')
route = new Route(router, '/:controller/:action(/:id)(.:format)',
route = new Route(router, '/:controller/:action/:id(.:format)', 'GET')
Pretty familiar to anyone who's used Merb/Rails - called by Router.match()
exports.Route =
class Route
constructor: ( router, path, method )->
if router && path
@match.apply this, arguments
the actual route builder function, mostly called by new Route
match: ( router, path, method, @optional=false )->
if typeof path != 'string'
throw new Error 'path must be a string'
is this a nested path?
if @path?
prefix = @path
get a list of key names in the new segment TODO: globs
new_keys = []
new_keys.push ':id' if @collection || @member # force id to be renamed for resources
Array::push.apply new_keys, path.match RegExp(KEY.source, 'g') # find ALL
rename earlier keys
for key in new_keys
replKey = new RegExp ":(#{key.substring(1)}/?)"
prefix = prefix.replace replKey, ":#{inflection.underscore inflection.singularize @params.controller}_$1"
return the new awesomeness
return new Route router, prefix+path, method
uppercase the method name
if typeof(method) == 'string'
@method = method.toUpperCase()
base properties
@params = {}
@parts = []
@route_name = null
@path = path
@router = router
Object.defineProperty this, "router", # exclude in enumerables
enumerable: false
configurable: false
writable: false
value: router
path parsing
while part = PARTS.exec path
@parts.push part
have to do this in two passes due to RegExp execution limits
for part, i in @parts
if OGRP.test part # optional group
@parts[i] = new Route @router, OGRP.exec(part)[1], true, true
else if KEY.test part # key
@parts[i] = new Key KEY.exec(part)[1]
else if GLOB.test part # glob
@parts[i] = new Glob GLOB.exec(part)[1]
else # string
@parts[i] = String part
unless @optional
@router.routes.push this
this
get: ( path )->
@match @router, path, 'GET'
put: ( path )->
@match @router, path, 'PUT'
post: ( path )->
@match @router, path, 'POST'
del: ( path )->
@match @router, path, 'DELETE'
generates standard resource routes for a controller name
route.resource('products')
returns a Resource object
resource: ( controller )->
new Resource this, controller
returns a composite regex string of all route parts
regexString: ->
ret = ''
a route regex is a composite of its parts' regexe(s|n)
for part in @parts
ret += if part.regexString then part.regexString() else regExpEscape part
"(#{ret})#{if @optional then '?' else ''}"
builds & tests on a full regex of the entire path
route.test( '/products/19/edit' )
=> true
returns true/false depending on whether the url matches
test: ( string )->
cache the regex string
@regex = RegExp "^#{@regexString()}(\\\?.*)?$" unless @regex
@regex.test string
defines the endpoint & mixes in optional params
route.to( 'controller.action' )
route.to( 'controller.action', {lang:'en'} )
returns the route for chaining
to: ( 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 new Error 'syntax should be in the form: controller.action'
@params.controller = endpoint[0]
@params.action = endpoint[1]
extra_params = {} unless kindof(extra_params) == 'object'
mixin @params, extra_params
this # chainable
just sets the route name - NAMED ROUTES ARE NOT CURRENTLY USED
route.name( 'login' )
route.name( 'homepage' ) # etc...
returns: the route for chaining
name: ( name )->
@route_name = name
this # chainable
sets conditions that each url variable must match for the URL to be valid
route.where( { id:/\d+/, username:/\w+/ } )
returns: the route for chaining
where: ( conditions )->
if kindof(conditions) != 'object'
throw new Error 'conditions must be an object'
for part in @parts
unless typeof part == 'string'
recursively apply all conditions to sub-parts
part.where conditions
this # chainable
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()
stringify: ( params )->
url = [] # urls start life as an array to enable a second pass
for part in @parts
if part instanceof Key || part instanceof Glob
if params[part.name]? && 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]
delete params[part.name]
# and remove from list of params
else if @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 (backwards, so later optionals are resolved first) for i=url.length-1; i>=0; i--
for part, i in url by -1
if part instanceof Route
part = part.stringify params # recursion is your friend
it resolved to a url fragment!
if part
params = part[1] # replace leftover params hash with the new, smaller leftover params hash
url[i] = part = part[0] # leave only the string for joining
else
delete url[i] # get rid of these shits
for key, val of @params
remove from leftovers, they're implied in the to() portion of the route
delete params[key]
[ url.join(''), params ]
keysAndRoutes: ->
@parts.filter (part)->
part instanceof Key || part instanceof Glob || part instanceof Route
keys: ->
@parts.filter (part)->
part instanceof Key || part instanceof Glob
parses a URL into a params object
route.parse( '/products/15/edit', 'GET' )
=> { controller:'products', action:'edit', id:15 }
returns: a params hash || false (if the route doesn't match)
this is meant to be called by Router.first() && Router.all()
parse: ( 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
url = require('url').parse urlParam # TODO: fix this
path = decodeURI url.pathname
params = { method: method }
mixin params, @params
route HEAD requests to GET endpoints
head = params.method == 'HEAD'
if head
params.method = 'GET' # we'll put it back when we're done
if the method doesn't match, gtfo immediately
if @method? && params.method?
if @method != params.method
return false
assign the route's method if there isn't one
params.method ?= @method
TODO: implement substring checks for possible performance boost
if the route doesn't match the regex, gtfo
return false unless @test path
parse the URL with the regex slice(2) returns just the array of matches from the regexp object
parts = new RegExp("^#{@regexString()}$").exec(path).slice(2)
keysAndRoutes = @keysAndRoutes()
pairings = []
loop 1 (forwards) - collect the key/route -> part pairings for loop 2
`
for (var i=0, j=0; i<keysAndRoutes.length; i++, j++) {
var part = parts[j]
, segm = keysAndRoutes[i]
// if this part doesnt match the segment, move on
if ( !segm.test(part) ) { j++; continue }
// stash the pairings for loop 2
pairings.push( [ segm, part ] )
// routes must advance the part iterator by the number of parts matched in the segment
if (segm instanceof Route) j+= part.match(segm.regexString()).slice(2).length || 0
}
`
loop 2 (backwards) - parse the key/route -> part pairings (backards so later optional matches are preferred) i.e. /path(.:format)/something(.:format) would keep the last format segment, while discarding the first this makes life easier for nesting route definitions
for pair in pairings by -1
part = pair[1]
segm = pair[0]
actually mixin the params
if segm instanceof Key || segm instanceof Glob
params[segm.name] = part
else if segm instanceof Route
mixin params, segm.parse(part, method)
replace HEAD method?
params.method = 'HEAD' if head
params
nest: ( cb )->
if typeof cb != 'function'
throw new Error 'route.nest() requires a callback function'
cb.call this
this # for chaining
toString: ->
right-pads strings
rpad = (str,len)->
ret = new Array len+1 # +1 for fenceposting
ret.splice 0, str.length, str
ret.join ' '
[
rpad( @method || 'ALL',8 ),
rpad( @path, 50 ),
[ @params.controller, @params.action ].join('.')
].join ''
deep object mixer
mixin = ( ret, mixins... )->
for obj in mixins
for own key, val of obj
if kindof(val) == 'object'
ret[key] = mixin {}, val
else
ret[key] = val
ret
better than typeof
kindof = ( o )->
switch
when typeof o != "object" then typeof o
when o == null then "null"
when o.constructor == Array then "array"
when o.constructor == Date then "date"
when o.constructor == RegExp then "regex"
else "object"
escape a string for literal embedding in a regexp
regExpEscape = do ->
specials = [ '/', '.', '*', '+', '?', '|', '(', ')', '[', ']', '{', '}', '\\' ]
sRE = new RegExp "(\\#{ specials.join '|\\' })", 'g'
( text )-> text.replace sRE, '\\$1'