sleeprest
Version:
Resource driven REST server.
261 lines (201 loc) • 7.03 kB
text/coffeescript
# Resource class
path = require 'path'
_ = require 'lodash'
Promise = require 'bluebird'
http = require 'http'
Request = require './request' # This is incorrect
Response = require './response'
RegExp.quote = (str) ->
str.replace /([.?*+^$[\]\\(){}|-])/g, "\\$1"
module.exports = class Resource
constructor: (options) ->
= []
= {}
= []
= options or {}
# Define /end/ function for a method on this resource
method: (method, uses..., fn) ->
m = method.toUpperCase()
if [m]?
throw new Error "Duplicate #{m} handler set!"
[m] = uses.concat [fn]
this # Chaining
# Add a method to run for ALL request (in/passing) this resource
use: (fn) ->
.push fn
this # Chaining
# Define a new resource point
resource: (args...) ->
# If it is a RegExp, do other things
if args[0] instanceof RegExp
[uriRegExp, parameters] = args
parameters ?= []
if parameters not instanceof Array
parameters = [parameters]
# Else, generate the regexp and parameters from url
else
if not args[0]? or args[0] is ''
throw new Error "First argument of .resource must be a non-empty string!"
uri = path.join args...
# Remove slash in front
if uri.indexOf('/') is 0
uri = uri.slice 1
# Split all seperate segments
parameters = []
PARAMREG = /([^/]+)/
uri = '^\\/' + uri.split('/').map (segment) ->
if segment.indexOf(':') is 0 # Parameter
parameters.push segment.slice(1)
PARAMREG.source
else # Just a string literal
RegExp.quote segment
.join('\\/') # Join with escaped slashes
uriRegExp = new RegExp uri
resource = new Resource
.push
resource: resource
uriRegExp: uriRegExp
paramNames: parameters
resource
# Alias for ::resource
res: @::resource
# Get request from listening or from a parent resource
_bubble: (req, response) ->
# Enable/Disable hall if this resource is supposed to
for key,value of
response.options[key] = value
url = req.url.match(/^([^?]*)\??.*$/)[1]
#console.log '\nGot a request:', req
promise = Promise.bind(response).return(req)
.forEach (use) ->
promise = promise.then(use).return(req)
promise.then =>
# Check resources for a match
matching = _.find , (resource) ->
url.match(resource.uriRegExp)?
# If no match, error!
if not matching?
# Check if it could be for this resource
if ['', '/'].indexOf(url) isnt -1
# Request is for this resource!
if not [req.method]?
throw new Error "Method not allowed!"
promise = Promise.bind(response)
[req.method].forEach (fn) ->
promise = promise.return(req).then(fn)
return promise
# If not, DIE!
throw new Error "Resource not found!"
match = url.match matching.uriRegExp # Yes, I know
# Get the params
params = req.params ||= {} # Get the already set params (or make a new empty object)
for paramname, key in matching.paramNames
params[paramname] = match[key+1]
# Update the url
req.url = url.slice match[0].length
# Let it bubble up!
matching.resource._bubble req, response
# First bubble in a chain, does some extra stuff
_forgeRequest: (req) ->
req._url = req.url ?= '/' # Save origional url
req.method ?= 'GET'
# TODO: Check for existance of the resource:method first
response = new Response req, # Placeholder for the real response
response.setBody Promise.try(, [req, response], this)
# Set a certain option in the resource.
# Subresources may overwrite this, as this also may overwrite super resources.
set: (option, value) ->
if not value?
delete [option]
else
[option] = value
# Change this resource into a server
listen: (args...) ->
= http.createServer (req, res) =>
# Go up the tree, all the way
.then (response) ->
response.applyTo res
.listen args...
['get', 'post', 'put', 'delete'].forEach (method) ->
Resource::[method] = (args...) ->
method, args...
# PLUGINS, WILL MOVE THEM TO THEIR OWN FILE
formidable = require 'formidable'
Resource.multipartBodyParser = (opts) ->
opts ?= {}
(req) ->
if req.contentType isnt "multipart/form-data" # or (req.getContentLength() is 0 and not req.isChunked())
return
form = new formidable.IncomingForm()
###
form.keepExtensions = (if opts.keepExtensions then true else false)
form.onPart = onPart = (part) ->
if part.filename and options.multipartFileHandler
options.multipartFileHandler part, req
else if not part.filename and options.multipartHandler
options.multipartHandler part, req
else
form.handlePart part
###
new Promise (yell, cry) ->
form.parse req, (err, fields, files) ->
if err?
return cry err
yell [fields, files]
.spread (fields, files) ->
req.body = fields
req.files = files
.catch (err) ->
#new BadRequestError(err.message)
throw err
querystring = require("querystring")
Resource.formBodyParser = ->
(req) ->
if req.contentType isnt 'application/x-www-form-urlencoded'
return
req.loadBody().then (body) ->
params = querystring.parse body.toString()
#req._body = req.body
req.body = params
.catch (e) ->
#throw new errors.InvalidContentError(e.message)
throw new Error "Invalid content"
Resource.jsonBodyParser = ->
(req) ->
if req.contentType isnt 'application/json'
return
if ['POST', 'PUT'].indexOf(req.method) is -1
return
req.loadBody().then (body) ->
try
req.body = JSON.parse body
catch e
console.log 'JSON ERROR :\'(', e
Resource.bodyParser = ->
json = Resource.jsonBodyParser()
multipart = Resource.multipartBodyParser()
form = Resource.formBodyParser()
(req) ->
Promise.all [
form(req),
json(req),
multipart(req)
]
Resource.authorizationParser = (options) ->
(req) ->
# Fill to prevent /nullpoint/ errors
req.authorization = {}
unless req.headers.authorization
return
[scheme, credentials] = req.headers.authorization.split " ", 2
if not credentials?
throw new Error "HTTP:400 Invalid Authorization header!"
# Fill the authorization
req.authorization =
scheme: scheme.toLowerCase()
credentials: credentials
raw: scheme.toLowerCase() + " " + credentials
# Long ago, there was a code to parse Basic auth.
# But then the basic auth was to basic, so it went home.
# The code had no use anymore, and commited suicide.
# Rest in peace, basic auth parse code.