UNPKG

14.3 kBtext/coffeescriptView Raw
1# Lazorse: lazy resources, lasers, and horses.
2
3METHODS = ['DELETE', 'GET', 'HEAD', 'PATCH', 'POST', 'PUT', 'OPTIONS']
4
5require './uri-template-matchpatch'
6errors = require './errors'
7parser = require 'uri-template'
8connect = require 'connect'
9# Used for loading example request JSON files
10{readFileSync} = require 'fs'
11
12###
13The main export is a function that constructs a ``LazyApp`` instance and
14starts it listening on the port defined by the apps ``port`` property (default
15is 3000)
16###
17module.exports = exports = (builder) ->
18 app = new LazyApp builder
19 server = connect.createServer()
20 server.use connect.favicon()
21 server.use connect.logger()
22 server.use connect.bodyParser()
23 server.use app
24 server.listen app.port
25 server
26
27###
28The module also exports a function that constructs an app without starting a
29server
30###
31exports.app = (builder) -> new LazyApp builder
32
33###
34The main application class groups together five connect middleware:
35
36 :meth:`lazorse::LazyApp.findResource`
37 Finds the handler function for a request.
38
39 :meth:`lazorse::LazyApp.coerceParams`
40 Validates/translates incoming URI parameters into objects.
41
42 :meth:`lazorse::LazyApp.dispatchHandler`
43 Calls the handler function found by the router.
44
45 :meth:`lazorse::LazyApp.renderResponse`
46 Writes data back to the client.
47
48 :meth:`lazorse::LazyApp.handleErrors`
49 Handles known error types.
50
51Each of these methods is bound to the ``LazyApp`` instance, so they can be used
52as standalone middleware without needing to wrap them in another callback.
53###
54class LazyApp
55 ###
56 The constructor takes a `builder` function as it's sole argument. This
57 function will be called in the context of the app object `before` the default
58 index and examples resources are created. The builder can change the location
59 of these resources by setting ``@indexPath`` and ``@examplePath``, or disable
60 them by setting the path to ``false``.
61 ###
62 constructor: (builder) ->
63 app = @ # Needed by some of the callbacks defined here
64
65 # Defaults
66 @port = 3000
67 @renderers = {}
68 @renderers[type] = func for type, func of require './render'
69
70 @errors = {}
71 @errorHandlers = {}
72 @errors[name] = err for name, err of errors.types
73 @errorHandlers[name] = handler for name, handler of errors.handlers
74
75 # handleErrors must be manually rebound to preserve it's arity.
76 # Furthermore, the Function.bind in node ~0.4.12 doesn't preserve arity
77 _handleErrors = @handleErrors
78 @handleErrors = (err, req, res, next) -> _handleErrors.call app, err, req, res, next
79
80 @passErrors = false
81 @helpers =
82 ok: (data) ->
83 @res.statusCode = 200
84 @res.data = data
85 @next()
86 data: (err, data) -> return @next err if err?; @ok data
87 link: (name, ctx) -> app.resourceIndex[name].template.expand(ctx or @)
88 error: (name, args...) ->
89 if 'function' == typeof name
90 @next new name args...
91 else if @app.errors[name]?
92 @next new @app.errors[name](args...)
93 else
94 @next name
95
96 # Internal state
97 @resourceIndex = {}
98 @coercions = {}
99 @coercionDescriptions = {}
100 @routes = {}
101 @routes[method] = [] for method in METHODS
102
103 @_prefix = ''
104 @_stack = [@findResource, @coerceParams, @dispatchHandler, @renderResponse]
105
106 # Call the builder before installing default resources so it can override
107 # the index and examples path.
108 builder.call @ if 'function' is typeof builder
109
110 indexPath = @indexPath ? '/'
111 examplePath = @examplePath ? '/examples'
112 parameterPath = @parameterPath ? '/parameters'
113
114 defaultResources = {}
115 if indexPath
116 defaultResources['/'] =
117 description: "Index of all resources"
118 GET: ->
119 specs = for shortName, resource of app.resourceIndex
120 {template, shortName, description} = resource
121 methods = (k for k of resource when k in METHODS)
122 template = String template
123 spec = {shortName, description, methods, template}
124 if resource.examples and examplePath
125 spec.examples = @link 'exampleRequests', {shortName}
126 spec
127 specs.sort (a, b) ->
128 [a, b] = (s.template for s in [a, b])
129 return 0 if a == b
130 if a < b then -1 else 1
131 @ok specs
132
133 if examplePath
134 defaultResources[examplePath+'/{shortName}'] =
135 shortName: 'exampleRequests'
136 description: "Example requests for a named resource"
137 GET: ->
138 unless resource = app.resourceIndex[@shortName]
139 return @error 'NotFound', 'resource', @shortName
140 unless resource.examples
141 return @error 'NotFound', 'Examples for resource', @shortName
142 examples = for example in resource.examples
143 ex = method: example.method, path: @link @shortName, example.vars
144 ex.body = example.body if example.body?
145 ex
146 @ok examples
147
148 if parameterPath
149 defaultResources[parameterPath+'/'] = GET: -> @ok app.coercionDescriptions
150 defaultResources[parameterPath+'/{parameterName}'] =
151 shortName: 'parameterDocumentation'
152 description: "The documentation for a specific template parameter."
153 GET: ->
154 unless (coercion = app.coercions[@parameterName])
155 return @error 'NotFound', 'parameters', @parameterName
156 @ok app.coercionDescriptions[@parameterName]
157
158 @resource defaultResources
159
160 ###
161 Register one or more resources. The ``specs`` object should map URI templates to
162 an object describing the resource. For example::
163
164 @resource '/{category}/{thing}':
165 shortName: "nameForClientsAndDocumentation"
166 description: "a longer description"
167 GET: -> ...
168 POST: -> ...
169 PUT: -> ...
170 examples: [
171 {method: 'GET', vars: {category: 'cats', thing: 'jellybean'}}
172 ]
173 ###
174 resource: (specs) ->
175 for template, spec of specs
176 if spec.shortName and @resourceIndex[spec.shortName]?
177 throw new Error "Duplicate short name '#{spec.shortName}'"
178
179 spec.template = parser.parse @_prefix + template
180 @resourceIndex[spec.shortName] = spec if spec.shortName
181 for method in METHODS when handler = spec[method]
182 @routes[method].push spec
183
184 ###
185 Register one or more helper functions. The ``helpers`` parameter should be an
186 object that maps helper names to callback functions.
187
188 The helpers will be made available in the context used by coercions and
189 request handlers (see :meth:`lazorse::LazyApp.buildContext`). So if you
190 register a helper named 'fryEgg' it will be available as ``@fryEgg``.
191 ###
192 helper: (helpers) ->
193 for name, helper of helpers
194 @helpers[name] = helper
195
196 ###
197 Register a new template parameter coercion with the app.
198
199 :param name: The name of the template parameter to be coerced.
200 :param description: A documentation string for the parameter name.
201 :param coercion: a ``(value, next) -> next(err, coercedValue)`` function that
202 will be called with ``this`` set to the request context. If not given, the
203 value will be passed through unchanged (useful for documentation purposes).
204
205 See :rst:ref:`coercions` in the guide for an example.
206 ###
207 coerce: (name, description, coercion) ->
208 throw new Error "Duplicate coercion name: #{name}" if @coercions[name]?
209 @coercionDescriptions[name] = description
210 @coercions[name] = coercion or (v, n) -> n(null, v)
211
212 ###
213 Register an error type with the app. The callback wlll be called by
214 ``@errorHandler`` when an error of this type is encountered.
215
216 Note that this *requires* named functions, so in coffeescript this means
217 using classes.
218
219 Additionally, errors of this type will be available to the @error helper in
220 handler/coercion callback by it's stringified name.
221
222 See :rst:ref:`named errors <named-errors>` in the guide for an example.
223 ###
224 error: (errType, cb) ->
225 errName = errType.name
226 @errors[errName] = errType
227 @errorHandlers[errName] = cb if cb?
228
229
230 ###
231 Register a new renderer function with the app. Can be supplied with two
232 parameters: a content-type and renderer function, or an object mapping
233 content-types to rendering functions.
234
235 See :rst:ref:`Rendering` in the guide for an example of a custom renderer.
236 ###
237 render: (contentType, renderer) ->
238 if typeof contentType is 'object'
239 @renderers[ct] = r for ct, r of contentType
240 else
241 @renderers[contentType] = renderer
242
243 ###
244 Call ``mod.include`` in the context of the app. The (optional) ``path``
245 parameter will be prefixed to all resources defined by the include.
246 ###
247 include: (path, mod) ->
248 if typeof path.include == 'function'
249 mod = path
250 path = ''
251 if typeof mod.include != 'function'
252 throw new Error "#{mod} does not have a .include method"
253 restorePrefix = @_prefix
254 @_prefix = path
255 mod.include.call @
256 @_prefix = restorePrefix
257
258 ###
259 Find the first matching resource template for the request, and assign it to
260 ``req.resource``
261
262 `Connect middleware, remains bound to the app object.`
263 ###
264 findResource: (req, res, next) =>
265 try
266 i = 0
267 resources = @routes[req.method]
268 nextHandler = (err) =>
269 return next err if err? and err != 'resource'
270 r = resources[i++]
271 return next(new @errors.NotFound 'resource', req.url) unless r?
272 vars = r.template.match req.url
273 return nextHandler() unless vars
274 req.resource = r
275 req.vars = vars
276 next()
277 nextHandler()
278 catch err
279 next err
280
281 ###
282 Walk through ``req.vars`` call any registered coercions that apply.
283
284 `Connect middleware, remains bound to the app object.`
285 ###
286 coerceParams: (req, res, next) =>
287 return next() unless req.vars
288 ctx = @buildContext req, res, next
289 varNames = (k for k in Object.keys req.vars when @coercions[k]?)
290 return next() unless varNames.length
291 varNames.sort (a, b) -> req.url.indexOf(a) - req.url.indexOf(b)
292 i = 0
293 nextCoercion = =>
294 name = varNames[i++]
295 return next() unless name?
296 coercion = @coercions[name]
297 coercion.call ctx, req.vars[name], (e, newValue) ->
298 return next e if e?
299 #if e == 'drop' then delete req.vars[name] else
300 req.vars[name] = newValue
301 nextCoercion()
302 nextCoercion()
303
304
305 ###
306 Calls the handler function for the matched resource if it exists.
307
308 `Connect middleware, remains bound to the app object.`
309 ###
310 dispatchHandler: (req, res, next) =>
311 return next() unless req.resource?
312 ctx = @buildContext req, res, next
313 # the resource handler should call next()
314 req.resource[req.method].call ctx, ctx
315
316 ###
317 Renders the data in ``req.data`` to the client.
318
319 Inspects the ``accept`` header and falls back to JSON if
320 it can't find a type it knows how to render. To install or override the
321 renderer for a given content/type use :meth:`lazorse::LazyApp.render`
322
323 `Connect middleware, remains bound to the app object.`
324 ###
325 renderResponse: (req, res, next) =>
326 return next new @errors.NotFound if not req.resource
327 return next new @errors.NoResponseData if not res.data
328 if req.headers.accept and [types, _] = req.headers.accept.split ';'
329 for type in types.split ','
330 if @renderers[type]?
331 res.setHeader 'Content-Type', type
332 return @renderers[type] req, res, next
333 # Fall back to JSON
334 res.setHeader 'Content-Type', 'application/json'
335 @renderers['application/json'] req, res, next
336
337 ###
338 Intercept known errors types and return an appropriate response. If
339 ``@passErrors`` is set to false (the default) any unknown error will send
340 a generic 500 error.
341
342 `Connect middleware, remains bound to the app object.`
343 ###
344 handleErrors: (err, req, res, next) ->
345 errName = err.constructor.name
346 if @errorHandlers[errName]?
347 @errorHandlers[errName](err, req, res, next)
348 else if @passErrors and not (err.code and err.message)
349 next err, req, res
350 else
351 res.statusCode = err.code or 500
352 message = 'string' == typeof err and err or err.message or "Internal error"
353 res.data = error: message
354 # @renderer will re-error if req.resource isn't set (e.g. no resource matched)
355 req.resource ?= true
356 @renderResponse req, res, next
357
358 ###
359 .. include:: handler_context.rst
360 ###
361 buildContext: (req, res, next) ->
362 ctx = {req, res, next, app: this}
363 vars = req.vars
364 for n, h of @helpers
365 ctx[n] = if 'function' == typeof h then h.bind vars else h
366 vars.__proto__ = ctx
367 vars
368
369 ###
370 Insert one or more connect middlewares into this apps internal stack.
371
372 :param existing: The middleware that new middleware should be inserted in
373 front of.
374
375 :param new_middle: The new middleware to insert. This can be *either* one or
376 more middleware functions, *or* a string name of a connect middleware and
377 additional parameters for that middleware.
378
379 Examples::
380
381 @before @findResource, (req, res, next) ->
382 res.setHeader 'X-Nihilo', ''
383 next()
384
385 @before @findResource, 'static', __dirname + '/public'
386
387 ###
388 before: (existing, new_middle...) ->
389 i = @_stack.indexOf(existing)
390 if i < 0
391 throw new Error "Middleware #{existing} does not exist in the app"
392
393 # If we receive an array, add each sub-array in order
394 if Array.isArray(new_middle[0])
395 for nm in new_middle[0]
396 @before existing, nm...
397 return
398
399 if typeof new_middle[0] is 'string'
400 [name, args...] = new_middle
401 if not connect[name]?
402 throw new Error "Can't find middleware by name #{name}"
403 new_middle = [ connect[name](args...) ]
404 @_stack.splice i, 0, new_middle...
405
406 ###
407 Extend a connect server with the default middleware stack from this app
408 ###
409 extend: (server) ->
410 server.use mw for mw in @_stack
411
412 ###
413 Act as a single connect middleware
414 ###
415 handle: (req, res, goodbyeLazorse) ->
416 stack = (mw for mw in @_stack)
417 nextMiddleware = =>
418 mw = stack.shift()
419 return goodbyeLazorse() unless mw?
420 mw req, res, (err) =>
421 return @handleErrors err, req, res, goodbyeLazorse if err?
422 nextMiddleware()
423 nextMiddleware()
424
425# vim: set et: