1 |
|
2 |
|
3 | METHODS = ['DELETE', 'GET', 'HEAD', 'PATCH', 'POST', 'PUT', 'OPTIONS']
|
4 |
|
5 | require './uri-template-matchpatch'
|
6 | errors = require './errors'
|
7 | parser = require 'uri-template'
|
8 | connect = require 'connect'
|
9 |
|
10 | {readFileSync} = require 'fs'
|
11 |
|
12 |
|
13 |
|
14 |
|
15 |
|
16 |
|
17 | module.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 |
|
28 |
|
29 |
|
30 |
|
31 | exports.app = (builder) -> new LazyApp builder
|
32 |
|
33 |
|
34 |
|
35 |
|
36 |
|
37 |
|
38 |
|
39 |
|
40 |
|
41 |
|
42 |
|
43 |
|
44 |
|
45 |
|
46 |
|
47 |
|
48 |
|
49 |
|
50 |
|
51 |
|
52 |
|
53 |
|
54 | class LazyApp
|
55 | |
56 |
|
57 |
|
58 |
|
59 |
|
60 |
|
61 |
|
62 | constructor: (builder) ->
|
63 | app = @
|
64 |
|
65 |
|
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 |
|
76 |
|
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 |
|
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 |
|
107 |
|
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 |
|
162 |
|
163 |
|
164 |
|
165 |
|
166 |
|
167 |
|
168 |
|
169 |
|
170 |
|
171 |
|
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 |
|
186 |
|
187 |
|
188 |
|
189 |
|
190 |
|
191 |
|
192 | helper: (helpers) ->
|
193 | for name, helper of helpers
|
194 | @helpers[name] = helper
|
195 |
|
196 | |
197 |
|
198 |
|
199 |
|
200 |
|
201 |
|
202 |
|
203 |
|
204 |
|
205 |
|
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 |
|
214 |
|
215 |
|
216 |
|
217 |
|
218 |
|
219 |
|
220 |
|
221 |
|
222 |
|
223 |
|
224 | error: (errType, cb) ->
|
225 | errName = errType.name
|
226 | @errors[errName] = errType
|
227 | @errorHandlers[errName] = cb if cb?
|
228 |
|
229 |
|
230 | |
231 |
|
232 |
|
233 |
|
234 |
|
235 |
|
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 |
|
245 |
|
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 |
|
260 |
|
261 |
|
262 |
|
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 |
|
283 |
|
284 |
|
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 |
|
300 | req.vars[name] = newValue
|
301 | nextCoercion()
|
302 | nextCoercion()
|
303 |
|
304 |
|
305 | |
306 |
|
307 |
|
308 |
|
309 |
|
310 | dispatchHandler: (req, res, next) =>
|
311 | return next() unless req.resource?
|
312 | ctx = @buildContext req, res, next
|
313 |
|
314 | req.resource[req.method].call ctx, ctx
|
315 |
|
316 | |
317 |
|
318 |
|
319 |
|
320 |
|
321 |
|
322 |
|
323 |
|
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 |
|
334 | res.setHeader 'Content-Type', 'application/json'
|
335 | @renderers['application/json'] req, res, next
|
336 |
|
337 | |
338 |
|
339 |
|
340 |
|
341 |
|
342 |
|
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 |
|
355 | req.resource ?= true
|
356 | @renderResponse req, res, next
|
357 |
|
358 | |
359 |
|
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 |
|
371 |
|
372 |
|
373 |
|
374 |
|
375 |
|
376 |
|
377 |
|
378 |
|
379 |
|
380 |
|
381 |
|
382 |
|
383 |
|
384 |
|
385 |
|
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 |
|
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 |
|
408 |
|
409 | extend: (server) ->
|
410 | server.use mw for mw in @_stack
|
411 |
|
412 | |
413 |
|
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 |
|