{ Key, Glob } = require './key'
{ Text } = require './text'
{ Resource } = require './resource'
{ kindof, mixin } = require './helpers'
inflection = require 'inflection'
{ Key, Glob } = require './key'
{ Text } = require './text'
{ Resource } = require './resource'
{ kindof, mixin } = require './helpers'
inflection = require 'inflection'
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, @optional=false )->
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
new_keys.push *path.match RegExp Key.regex.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"
create a new route instance
nested_route = new Route router, prefix+path, method, @optional
add local default params
nested_route.default_params = @default_params
apply local conditions
nested_route.where @conditions if @conditions
return the new awesomeness
return nested_route
uppercase the method name
if typeof(method) == 'string'
@method = method.toUpperCase()
base properties
@params = {}
@default_params = {}
@parts = []
@route_name = null
@path = path
@router = router
Object.defineProperty this, 'router', # exclude in enumerables
enumerable: false
configurable: false
writable: false
value: router
@parts = Route.parse router, path, method, @optional
reset the path to the generated one (chop off any extra )’s )
@path = @toString()
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'
patch: ( path )-> @match @router, path, 'PATCH'
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: ->
a route regex is a composite of its parts’ regexe(s|n)
ret = ['(']
ret.push part.regexString() for part in @parts
ret.push ')'
ret.push '?' if @optional
ret.join ''
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()}(\\\?.*)?$"
@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, default_params )->
if !default_params && typeof endpoint != 'string'
[ default_params, endpoint ] = [ endpoint, undefined ]
mixin @default_params, default_params
TODO: make endpoint optional, since you can have the controller & action in the URL utself, even though that’s a terrible idea…
if endpoint
unless 0 < endpoint.indexOf '.'
throw new Error 'syntax should be in the form: controller.action'
[ @params.controller, @params.action ] = endpoint.split '.'
mixin @params, @default_params
this # chainable
sets the route name - NAMED ROUTES ARE NOT CURRENTLY USED
route.as( 'login' )
route.as( 'homepage' ) # etc...
returns: the route for chaining
as: ( @route_name )->
this # chainable
alias for as
name: ( name )->
console.log '''
DEPRECATION NOTICE: this method has been renamed "as"
and will be removed in a future version of Barista
'''
@as.apply this, arguments
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'
recursively apply all conditions to sub-parts
part.where? @conditions for part in @parts
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
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 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: ->
part for part in @parts when part instanceof Key || part instanceof Route
keys: ->
part for part in @parts when part instanceof Key
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
if is_head_req = params.method == 'HEAD'
params.method = 'GET' # we'll put it back when we're done
if the method doesn’t match, gtfo immediately
return false if @method? && params.method? && @method != params.method
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 first 2 elements are shit, chop ‘em
parts = new RegExp("^#{@regexString()}$").exec(path)[2..]
pairings = []
loop 1 (forwards) - collect the key/route -> part pairings for loop 2
j = 0
for segm in @keysAndRoutes()
part = parts[j]
stash the pairings for loop 2 if this part matches segment
pairings.push [ segm, part ] if segm.test part
routes must advance the part iterator by the number of parts matched in the segment
j+= segm.regex.exec(part||'')[2..].length || 1
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 [ segm, part ] in pairings by -1
actually mixin the params
if segm instanceof Key
params[segm.name] = part
else if segm instanceof Route
mixin params, segm.parse part, method
replace HEAD method?
params.method = 'HEAD' if is_head_req
params
nest: ( cb )->
unless typeof cb == 'function'
throw new Error 'route.nest() requires a callback function'
cb.call this
this # for chaining
returns the original route definition
toString: ->
defn = (part.toString() for part in @parts).join ''
return "(#{ defn })" if @optional
defn
parses a route definition into a swiss army bazooka
route = Route.parse(router, '/:controller/:action/:id(.:format)')
route = Route.parse(router, '/:controller/:action(/:id)(.:format)', 'GET')
route = Route.parse(router, '/:controller/:action(/:id)(.:format)',
route = Route.parse(router, '/:controller/:action/:id(.:format)', 'GET')
Pretty familiar to anyone who’s used Merb/Rails - called by Router.match()
@parse = ( router, string, method, optional=false )->
parts = []
parse it char by char, baby
i = 0
len = string.length
for char, i in string
while i < len
char = string[i]
rest = string[i..]
special consideration for Ogrp starts
if char == '(' && i == 0
i++
continue # skip this char, since it's not strictly part of the route definition
the route definition is over, return what we have
if char == ')'
return parts
if char == ':' && string[i+1] != ':'
parts.push part = Key.parse rest
else if char == '*'
parts.push part = Glob.parse rest
else if char == '('
parts.push part = new Route router, rest, method, true
else
parts.push part = Text.parse rest
i += part.toString().length
parts