1 |
|
2 |
|
3 | METHODS = ['DELETE', 'GET', 'HEAD', 'PATCH', 'POST', 'PUT']
|
4 |
|
5 | require './uri-template-matchpatch'
|
6 | errors = require './errors'
|
7 | parser = require 'uri-template'
|
8 |
|
9 | {readFileSync} = require 'fs'
|
10 |
|
11 |
|
12 |
|
13 |
|
14 |
|
15 |
|
16 | module.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 |
|
27 |
|
28 |
|
29 |
|
30 | exports.app = (builder) -> new LazyApp builder
|
31 |
|
32 |
|
33 |
|
34 |
|
35 |
|
36 |
|
37 |
|
38 |
|
39 |
|
40 |
|
41 |
|
42 |
|
43 |
|
44 |
|
45 |
|
46 |
|
47 |
|
48 |
|
49 |
|
50 |
|
51 |
|
52 |
|
53 | class LazyApp
|
54 | |
55 |
|
56 |
|
57 |
|
58 |
|
59 |
|
60 |
|
61 | constructor: (builder) ->
|
62 | app = @
|
63 |
|
64 |
|
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 |
|
75 |
|
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 |
|
96 | @routeIndex = {}
|
97 | @coercions = {}
|
98 | @routeTable = {}
|
99 | @routeTable[method] = [] for method in METHODS
|
100 |
|
101 | @_prefix = ''
|
102 |
|
103 |
|
104 |
|
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 |
|
145 |
|
146 |
|
147 |
|
148 |
|
149 |
|
150 |
|
151 |
|
152 |
|
153 |
|
154 |
|
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 |
|
169 |
|
170 |
|
171 |
|
172 |
|
173 |
|
174 |
|
175 | helper: (helpers) ->
|
176 | for name, helper of helpers
|
177 | @helpers[name] = helper
|
178 |
|
179 | |
180 |
|
181 |
|
182 |
|
183 |
|
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 |
|
192 |
|
193 |
|
194 |
|
195 |
|
196 |
|
197 |
|
198 |
|
199 |
|
200 |
|
201 |
|
202 | error: (errType, cb) ->
|
203 | errName = errType.name
|
204 | @errors[errName] = errType
|
205 | @errorHandlers[errName] = cb if cb?
|
206 |
|
207 |
|
208 | |
209 |
|
210 |
|
211 |
|
212 |
|
213 |
|
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 |
|
223 |
|
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 |
|
238 |
|
239 |
|
240 |
|
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 |
|
261 |
|
262 |
|
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 |
|
278 | req.vars[name] = newValue
|
279 | nextCoercion()
|
280 | nextCoercion()
|
281 |
|
282 |
|
283 | |
284 |
|
285 |
|
286 |
|
287 |
|
288 | dispatchHandler: (req, res, next) =>
|
289 | return next() unless req.route?
|
290 | ctx = @buildContext req, res, next
|
291 |
|
292 | req.route[req.method].call ctx, ctx
|
293 |
|
294 | |
295 |
|
296 |
|
297 |
|
298 |
|
299 |
|
300 |
|
301 |
|
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 |
|
312 | res.setHeader 'Content-Type', 'application/json'
|
313 | @renderers['application/json'] req, res, next
|
314 |
|
315 | |
316 |
|
317 |
|
318 |
|
319 |
|
320 |
|
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 |
|
333 | req.route ?= true
|
334 | @renderResponse req, res, next
|
335 |
|
336 | |
337 |
|
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 |
|
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 |
|
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 |
|