'use strict'
_ = require 'underscore'
Backbone = require 'backbone'
EventBroker = require 'chaplin/lib/event_broker'
Controller = require 'chaplin/controllers/controller'
utils = require 'chaplin/lib/utils'
module.exports = class Route'use strict'
_ = require 'underscore'
Backbone = require 'backbone'
EventBroker = require 'chaplin/lib/event_broker'
Controller = require 'chaplin/controllers/controller'
utils = require 'chaplin/lib/utils'
module.exports = class RouteBorrow the static extend method from Backbone.
@extend = Backbone.Model.extendMixin an EventBroker.
_.extend @prototype, EventBrokerTaken from Backbone.Router.
escapeRegExp = /[\-{}\[\]+?.,\\\^$|#\s]/g
optionalRegExp = /\((.*?)\)/g
paramRegExp = /(?::|\*)(\w+)/gAdd or remove trailing slash from path according to trailing option.
processTrailingSlash = (path, trailing) ->
switch trailing
when yes
path += '/' unless path[-1..] is '/'
when no
path = path[...-1] if path[-1..] is '/'
pathCreate a route for a URL pattern and a controller action e.g. new Route '/users/:id', 'users', 'show', { some: 'options' }
constructor: (@pattern, @controller, @action, options) ->Disallow regexp routes.
if typeof @pattern isnt 'string'
throw new Error 'Route: RegExps are not supported.
Use strings with :names and `constraints` option of route'Clone options.
@options = if options then _.extend({}, options) else {}Store the name on the route if given
@name = @options.name if @options.name?Don’t allow ambiguity with controller#action.
if @name and @name.indexOf('#') isnt -1
throw new Error 'Route: "#" cannot be used in name'Set default route name.
@name ?= @controller + '#' + @actionInitialize list of :params which the route will use.
@allParams = []
@requiredParams = []
@optionalParams = []Check if the action is a reserved name
if @action of Controller.prototype
throw new Error 'Route: You should not use existing controller ' +
'properties as action names'
@createRegExp()You’re frozen when your heart’s not open.
Object.freeze? thisTests if route params are equal to criteria.
matches: (criteria) ->
if typeof criteria is 'string'
criteria is @name
else
propertiesCount = 0
for name in ['name', 'action', 'controller']
propertiesCount++
property = criteria[name]
return false if property and property isnt this[name]
invalidParamsCount = propertiesCount is 1 and name in ['action', 'controller']
not invalidParamsCountGenerates route URL from params.
reverse: (params, query) ->
params = @normalizeParams params
return false if params is false
url = @patternFrom a params hash; we need to be able to return the actual URL this route represents. Iterate and replace params in pattern.
for name in @requiredParams
value = params[name]
url = url.replace ///[:*]#{name}///g, value
Replace optional params.
for name in @optionalParams
if value = params[name]
url = url.replace ///[:*]#{name}///g, value
Kill unfulfilled optional portions.
raw = url.replace optionalRegExp, (match, portion) ->
if portion.match /[:*]/g
""
else
portionAdd or remove trailing slash according to the Route options.
url = processTrailingSlash raw, @options.trailing
return url unless queryStringify query params if needed.
if typeof query is 'object'
queryString = utils.queryParams.stringify query
url += if queryString then '?' + queryString else ''
else
url += (if query[0] is '?' then '' else '?') + queryValidates incoming params and returns them in a unified form - hash
normalizeParams: (params) ->
if utils.isArray paramsEnsure we have enough parameters.
return false if params.length < @requiredParams.lengthConvert params from array into object.
paramsHash = {}
for paramName, paramIndex in @requiredParams
paramsHash[paramName] = params[paramIndex]
return false unless @testConstraints paramsHash
params = paramsHash
elsenull or undefined params are equivalent to an empty hash
params ?= {}
return false unless @testParams params
paramsTest if passed params hash matches current constraints.
testConstraints: (params) ->Apply the parameter constraints.
constraints = @options.constraints
if constraints
for own name, constraint of constraints
return false unless constraint.test params[name]
trueTest if passed params hash matches current route.
testParams: (params) ->Ensure that params contains all the parameters needed.
for paramName in @requiredParams
return false if params[paramName] is undefined
@testConstraints paramsCreates the actual regular expression that Backbone.History#loadUrl uses to determine if the current url is a match.
createRegExp: ->
pattern = @patternEscape magic characters.
pattern = pattern.replace(escapeRegExp, '\\$&')Keep accurate back-reference indices in allParams. Eg. Matching the regex returns arrays like [a, undefined, c] and each item needs to be matched to the correct named parameter via its position in the array.
@replaceParams pattern, (match, param) =>
@allParams.push paramProcess optional route portions.
pattern = pattern.replace optionalRegExp, @parseOptionalPortionProcess remaining required params.
pattern = @replaceParams pattern, (match, param) =>
@requiredParams.push param
@paramCapturePattern matchCreate the actual regular expression, match until the end of the URL, trailing slash or the begin of query string.
@regExp = ///^#{pattern}(?=\/?(?=\?|$))///
parseOptionalPortion: (match, optionalPortion) =>
Extract and replace params.
portion = @replaceParams optionalPortion, (match, param) =>
@optionalParams.push paramReplace the match (eg. :foo) with capturing groups.
@paramCapturePattern matchReplace the optional portion with a non-capturing and optional group.
"(?:#{portion})?"
replaceParams: (s, callback) =>Parse :foo and *bar, replacing via callback.
s.replace paramRegExp, callback
paramCapturePattern: (param) ->
if param.charAt(0) is ':'Regexp for :foo.
'([^\/\?]+)'
elseRegexp for *foo.
'(.*?)'Test if the route matches to a path (called by Backbone.History#loadUrl).
test: (path) ->Test the main RegExp.
matched = @regExp.test path
return false unless matchedApply the parameter constraints.
constraints = @options.constraints
if constraints
return @testConstraints @extractParams path
trueThe handler called by Backbone.History when the route matches. It is also called by Router#route which might pass options.
handler: (pathParams, options) =>
options = if options then _.extend {}, options else {}pathDesc may be either an object with params for reversing or a simple URL.
if typeof pathParams is 'object'
query = utils.queryParams.stringify options.query
params = pathParams
path = @reverse params
else
[path, query] = pathParams.split '?'
if not query?
query = ''
else
options.query = utils.queryParams.parse query
params = @extractParams path
path = processTrailingSlash path, @options.trailing
actionParams = _.extend {}, params, @options.paramsConstruct a route object to forward to the match event.
route = {path, @action, @controller, @name, query}Publish a global event passing the route and the params. Original options hash forwarded to allow further forwarding to backbone.
@publishEvent 'router:match', route, actionParams, optionsExtract named parameters from the URL path.
extractParams: (path) ->
params = {}Apply the regular expression.
matches = @regExp.exec pathFill the hash using param names and the matches.
for match, index in matches.slice(1)
paramName = if @allParams.length then @allParams[index] else index
params[paramName] = match
params