You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

324 lines
9.7 KiB

fs = require('fs')
path = require('path')
module.exports = (app, options = {}) ->
options.strict ?= true
options.overwriteRender ?= true
options.log ?= false
options.root ?= app.set('controllers') || process.cwd() + '/controllers'
options.sharedFolder ?= 'shared'
new Controllers app, options
class Controllers
constructor: (app, @options) ->
self = this
@_controllers = {}
# Pre-load all the controllers... one time hit so done sync
this.executeOnDirectory @options.root, (file) ->
ext = path.extname file
if ext == '.js' || ext == '.coffee'
reduced = file.replace ext, ''
controller = path.basename reduced
self._controllers[controller] = require reduced
if self.options.log
console.log "Controller '#{controller}' has been loaded"
# We are off to hijack the req.app.routes._route
# which is the point of contact of all our get/post/pull/etc methods.
# We will let the usual chain occur till the very last
# callback, and then we will make sure the controller and action
# are both defined, and then load up that controller/action.
# We have already cached the controllers to reduce require calls
originalRoute = app.routes._route
app.routes._route = (method, path, defaults, callbacks...) ->
# We might not have defaults
if 'function' == typeof defaults
callbacks.push defaults
defaults = null
if callbacks.length == 0
callbacks.push (req, res) ->
defaults ?= { }
holder = { }
# overwrite the callbacks to use this info
newCallbacks = (self.overwriteCallback c, holder) for c in callbacks
result = originalRoute.call app.routes, method, path, newCallbacks
# Extend the routing by adding defaults
holder.route = newRoute = result.routes[method][result.routes[method].length - 1]
for defkey, defvalue of defaults
key = self.getKeyInRoute defkey, newRoute
if key?
key.default = defvalue
else
# controller/action is a special case and we need to save it
if defkey == 'controller' or defkey == 'action'
newRoute[defkey] = defvalue
# If we have a key for controller/action that means they could be anything
for key in newRoute.keys when key.name == 'controller' or key.name == 'action'
newRoute[key.name] = '*'
return result
# Add all the corresponding helpers
this.addHelpers app
addReqHelpers: (req, res) ->
self = this
req.executeController = (controller, action, next) ->
if not controller? or not action?
throw new Error("executeController needs the controller and action specified")
# If we pass a next switch the controller/action back to our current one
if next?
currentC = req.controller
currentA = req.action
nextFunc = next
next = ->
req.controller = currentC
req.action = currentA
nextFunc.apply this, arguments
req.controller = controller
req.action = action
self._controllers[controller][action] req, res, next
addHelpers: (app) ->
self = this
app.dynamicHelpers {
controller: (req, res) ->
req.controller
action: (req, res) ->
req.action
getUrl: (req, res) ->
(controller, action, other, query) ->
if not action? or 'object' == typeof action
query = other
other = action
action = controller
controller = null
controller ?= req.controller
other ?= {}
other.controller = controller
other.action = action
query ?= {}
if not action? or not controller?
throw new Error("getUrl needs at minimum an action defined, but also takes a controller")
for route in app.routes.routes.get
if self.isMatchingPath other, route
# We have found a route that matches
# We are stepping through the keys backwards so that if
# any keys are found the rest MUST be displayed
# (that is... optional keys cannot be blank)
hasReplaced = false
result = route.path
for i in [route.keys.length-1..0]
key = route.keys[i]
def = key.default ? ''
replacement = other[key.name] ? def
if hasReplaced and replacement == ''
throw new Error("The optional parameter '#{key.name}' is required for this getUrl call as an parameter further down the path has been specified")
else
if not hasReplaced
if (not key.optional or replacement != def) and
hasReplaced = true
else
replacement = ''
# Do the replacement
regExp = new RegExp ":#{key.name}(\\?)?"
result = result.replace regExp, replacement
# Remove multiple slashes
result = result.replace /\/+/g, '/'
# Remove trailing slash... unless we are at root
if result != '/'
result = result.replace /\/+$/, ''
# Add in query strings
first = true
for key, value of query
if first
first = false
result = result + '?' + key
if value? and value != ''
result = result + '=' + value
else
result = result + '&' + key
if value? and value != ''
result = result + '=' + value
return result
throw new Error("Route could not be found that matches getUrl parameters, make sure to specify a valid controller, action and required parameters")
}
getKeyInRoute: (name, route) ->
for key in route.keys when key.name == name
return key
return null
isMatchingPath: (object, route) ->
# First check the controller and action
if route.controller != '*' and route.controller != object.controller
return false
if route.action != '*' and route.action != object.action
return false
# This is checking that all items in the object match with a key
for key, value of object when key != 'controller' and key != 'action'
if not (@getKeyInRoute key, route)?
return false
# This is checking all (required) keys have an object value
for key in route.keys when key.name != 'controller' and key.name != 'action'
if not key.optional and not object[key]?
return false
return true
overwriteCallback: (callback, routeHolder) ->
self = this
options = @options
(req, resp, next) ->
# Call the normal callback
callback req, resp, next
# Add helpers
self.addReqHelpers req, resp
# set the current route
route = routeHolder.route
# Go through our keys and if they have a default and the param value
# is not set make sure it is
for key in route.keys when not req.params[key.name]? and key.default?
req.params[key.name] = key.default
# Grab our current controller/action either from the route or use defaults
req.controller = req.params.controller ? route.controller
req.action = req.params.action ? route.action
if options.log
console.log 'Controller: ' + req.controller
console.log 'Action: ' + req.action
if options.strict
if not req.controller?
throw new Error("Is in strict mode and no controller specified")
if not req.action?
throw new Error("Is in strict mode and no action specified")
if req.controller? and req.action?
# We have a controller and an action - lets overwrite the res.render
# command so that we do not have to specify view names
if options.overwriteRender
self.overwriteRender req, resp
# Find the controller
controller = self._controllers[req.controller]
if not controller?
if options.log
console.log "Controller '#{req.controller}' could not be found"
next 'route'
return
# Execute the action
action = controller[req.action]
if not action?
if options.log
console.log "Action '#{req.action}' could not be found on controller '#{req.controller}' "
next 'route'
return
# Execute the controller with a nothing followup action
action req, resp, next
else
if options.log
console.log 'Controller or action was not specified, no action was called'
overwriteRender: (req, resp) ->
self = this
original = resp.render
# This is the root dir the render method uses
root = resp.app.set('views') || process.cwd() + '/views'
resp.render = (view, opts, fn, parent, sub) ->
# Allow for view to be empty
if 'object' == typeof view || 'function' == typeof view
sub = parent
parent = fn
fn = opts
opts = view
view = null
# The view defaults to the action
view ?= req.action
# Set the root directory as the controller directory
# if that doesnt work, try the shared directory
# disable hints because it comes up funny like
hasHints = resp.app.enabled 'hints'
resp.app.disable 'hints'
result = null
secondResult = null
reset = ->
if hasHints
resp.app.enable 'hints'
finalPass = (err, err2, str) ->
reset()
if err?
err = err + '\r\n\r\n' + err2
if fn?
fn err, str
else
if err?
req.next err
else
resp.send str
secondRender = (err, str) ->
if err?
# If the first render failed failed try getting view from 'shared'
secondResult = original.call resp, self.options.sharedFolder + '/' + view, opts, ((err2, str2) -> finalPass(err2, err, str2)), parent, sub
else
reset()
if fn?
fn err, str
else
resp.send str
result = original.call resp, req.controller + '/' + view, opts, secondRender, parent, sub
if secondResult?
result = secondResult
reset()
return result
executeOnDirectory: (dir, action) ->
fs.readdirSync(dir).forEach (file) ->
localpath = dir + '/' + file
stat = fs.statSync localpath
if stat and stat.isDirectory()
self.executeOnDirectory localpath, action
else
action localpath