Created
June 7, 2012 13:02
-
-
Save tobiash/2888696 to your computer and use it in GitHub Desktop.
Generic Pre/Post filters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
_ = 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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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