UNPKG

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