Skip to content

Instantly share code, notes, and snippets.

@tobiash
Created June 7, 2012 13:02
Show Gist options
  • Save tobiash/2888696 to your computer and use it in GitHub Desktop.
Save tobiash/2888696 to your computer and use it in GitHub Desktop.
Generic Pre/Post filters
_ = require("underscore")._
Q = require("q")
logger = require('./logger').logger(@)
util = require 'util'
prepost = require './prepost'
beget = require './util/beget'
# Use ExpressJS route implementation
Route = require("express/lib/router/route")
#
# Resource Controller for Socket.IO (and possibly others)
# =======================================================
#
# The resource controller can be defined in a special DSL:
#
# @path "/tables", ->
# @on "read", ->
#
# @path ":id", ->
# @on "read", ->
# @on "join", ->
#
# @pre "*:read", ->
#
class Controller
# Error that gets thrown when the controller does not find
# a suitable route.
#
class @RouteNotFound
constructor: (@req) ->
toString: ->
"No route found for request #{req.url}##{req.method}"
constructor: (builder) ->
# Exposed for testing
@routes = routes = []
# Prepostify Utility
prepostified = new prepost.PrepostifiedCan
# Tracks the path prefixes of the current scope
pathPrefixes = []
# Builds a path from the given string by prepending it
# with the current scope path
buildPath = (path) ->
pathPrefixes.concat(path).join ""
# Keeps a mapping from action strings to routes
# e.g.
# foo/bar/:id#read => Route(...)
#
actions = {}
# Creates a combined string from a path and action
actionName = (path, action) ->
buildPath(path) + "#" + action
# Prefixes all filter actions with the current scope path
scoped_filter_adder = (filter_adder) ->
(actions..., fn) ->
scoped_actions = (buildPath(action) for action in actions)
filter_adder.apply @, scoped_actions.concat(fn)
@pre = scoped_filter_adder(prepostified.pre)
@post = scoped_filter_adder(prepostified.post)
@can = scoped_filter_adder(prepostified.can)
@on = (method, path, handler) ->
if _.isFunction path
handler = path
path = ""
action = actionName(path, method)
prepostified.on action, handler
route = new Route method, buildPath(path), []
routes.push route
actions[action] = route
#
# Creates a scope for child routes
#
# @path "/foo", ->
# @on "/:id", ...
#
# => "/foo/:id"
#
@path = (path, fn) ->
context = beget @, {}
pathPrefixes.push path
fn.call context
pathPrefixes.pop()
#
# Dispatches the request to the controller. The request needs to contain
# * url
# * method
# The request is passed to the matching route handler.
# Returns a promise-
#
@dispatch = (request, context) ->
matchRoute = (request) ->
for name, route of actions
values = null
params = {}
if route.method is request.method and (values = route.match(request.url))?
for { name: key },i in route.keys
params[key] = values[i+1]
return [name, params]
throw new Controller.RouteNotFound(request)
Q.fcall ->
[actionName, params] = matchRoute(request)
request.params = params
prepostified.action(actionName, context)(request)
# Run initializer in context
builder?.call @
module.exports = Controller
Q = require "q"
_ = require("underscore")._
# Pre/Post filters
# ================
#
# A small library to add pre and post filters to functions. Filters
# and functions run asynchronously and use promises.
#
# Returns a promise to execute the given list of filters within the
# given context. The given argument will be used as initial data for
# the sequential execution of promises.
#
executeFilters = (filters, context, arg, extraArgs) ->
(filters or []).reduce((
(soFar, f) -> soFar.then (arg) -> f.apply context, withExtraArgs(arg, extraArgs)
), Q.resolve(arg))
#
# Returns a promise to execute the cans for a given event
# Other than filters, the can checks return a boolean and all receive
# the primary edited object (May depend on the event implementation
#
executeCan = (canFilters, context, obj) ->
assertTrue = (value) ->
throw "Can check failed on object" unless value
return obj
(canFilters or []).reduce(
( (soFar, f) ->
soFar.then(assertTrue).then(_.bind f, context)), Q.resolve(obj)).then(assertTrue)
withExtraArgs = (arg1, extraArgs) ->
if _.any extraArgs, ((arg) -> arg?)
[arg1].concat extraArgs
else
[arg1]
# http://stackoverflow.com/questions/5575609/javascript-regexp-to-match-strings-using-wildcards-and
globToRegexp = (glob) ->
specialChars = "\\^$*+?.()|{}[]"
regexChars = ["^"]
for i in [0..glob.length-1]
c = glob.charAt i
switch c
when '?'
regexChars.push '.'
when '*'
regexChars.push '.*'
else
if specialChars.indexOf(c) >= 0
regexChars.push "\\"
regexChars.push c
regexChars.push "$"
return new RegExp(regexChars.join(""))
filterFn = (pre, post, fn, context, extraArgs) ->
(input) ->
executeFilters(pre, context, input, extraArgs)
.then((data) -> fn.apply context, withExtraArgs(data, extraArgs))
.then((data) -> executeFilters(post, context, data, extraArgs))
#
# Returns a function to add filters to multiple actions
# and store them in a hash
#
filterAdder = (filters) ->
(actions..., fn) ->
matchers = (globToRegexp(matchStr) for matchStr in actions)
filters.push { match: matchers, fn: fn }
@
#
# Returns all filters that match a given action from a filter array
#
filtersFor = (action, filters) ->
result = []
for { match: matchers, fn: fn } in filters
if _.any(matchers, (m) -> m.test(action))
result.push fn
result
class Prepostified
constructor: (actions = {}) ->
preFilters = []
postFilters = []
# Adds a pre filter to the given actions
@pre = filterAdder preFilters
# Adds a post filter to the given actions
@post = filterAdder postFilters
@use = (actions..., m) ->
matchers = (globToRegexp(matchStr) for matchStr in actions)
preFilters.push { match: matchers, fn: m.pre }
postFilters.unshift { match: matchers, fn: m.post }
@
@on = (actionNames..., fn) ->
for name in actionNames
actions[name] = fn
@names = ->
_.keys actions
# Returns a function to run the given action
# and the attached filters
@action = (actionName, context, extraArgs) ->
pre = filtersFor(actionName, preFilters)
post = filtersFor(actionName, postFilters)
filterFn pre, post,
(actions[actionName] or ->),
context,
extraArgs
class PrepostifiedCan extends Prepostified
constructor: (actions = {}) ->
super
canFilters = []
@can = filterAdder canFilters
superAction = @action
@action = (actionName, context) ->
cans = filtersFor(actionName, canFilters)
superAction actionName, context, cans
module.exports =
filterFn: filterFn
Prepostified: Prepostified
PrepostifiedCan: PrepostifiedCan
Controller = require './controller'
db = require "./db"
logger = require("./logger").logger()
tables = require("./model/tables")
module.exports = new Controller ->
# Log all requests
@pre "*", (req) ->
logger.debug "Handling request", req
req
@post "*", (res) ->
logger.debug "Sending response", res
res
@path "/tables", ->
@on "read", (req) ->
tables.allAvailable(@user.id)
@on "create", (req) ->
tables.create(@user.id, req.data)
@path "/:id", ->
@pre "*", (req) ->
@table = tables.get(req.params.id)
req
@on "read", (req) -> @table.toJSON()
@on "join", (req) ->
logger.debug "#{@user.name()} wants to join #{@table.id}"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment