neft
Version:
Universal Platform
424 lines (340 loc) • 13.5 kB
text/coffeescript
# Route
'use strict'
utils = require 'src/utils'
assert = require 'src/assert'
log = require 'src/log'
Schema = require 'src/schema'
Networking = require 'src/networking'
Document = require 'src/document'
Renderer = require 'src/renderer'
Dict = require 'src/dict'
log = log.scope 'App', 'Route'
module.exports = (app) -> class Route
if utils.isNode
usedTemplates = []
else
templates = Object.create null
lastClientHTMLRoute = null
## *Document* Route.getTemplateView(*String* viewName)
= do ->
if utils.isNode
(name) ->
scope = app: app, routes: new Dict
tmpl = app.views[name].render null, scope
usedTemplates.push tmpl
tmpl
else
(name) ->
scope = app: app, routes: new Dict
templates[name] ?= app.views[name].render null, scope
## Route::constructor(*Object* options)
Access it with:
```javascript
module.exports = function(app) {
var Route = app.Route;
};
```
Acceptable syntaxes:
```javascript
*Route* app.Route(*String* method, *String* uri, *Object* options)
*Route* app.Route(*String* methodWithUri, *Function* getData)
*Route* app.Route(*String* methodWithUri, *Object* options)
*Route* app.Route(*String* uri, *Function* getData)
*Route* app.Route(*String* uri, *Object* options)
*Route* app.Route(*String* method, *String* uri)
*Route* app.Route(*String* uri)
*Route* app.Route(*String* methodWithUri)
```
constructor: (method, uri, opts) ->
if utils.isObject(method)
# opts
opts = method
else if utils.isObject(uri)
opts = uri
else unless utils.isObject(opts)
opts = {}
if typeof method is 'string' and typeof uri isnt 'string'
opts.uri = method
else if typeof method is 'string' and typeof uri is 'string'
opts.method ?= method
opts.uri ?= uri
if typeof uri is 'function'
opts.getData ?= uri
assert.isObject opts
opts = utils.clone opts
if typeof opts.uri is 'string'
# support methodWithUri e.g. 'get /home'
spaceIndex = opts.uri.indexOf ' '
if spaceIndex isnt -1
opts.method ?= opts.uri.slice 0, spaceIndex
opts.uri = opts.uri.slice spaceIndex+1
opts.uri = new Networking.Uri opts.uri
assert.instanceOf opts.uri, Networking.Uri
opts.method ?= 'get'
assert.isString opts.method
opts.method = opts.method.toLowerCase()
assert.ok utils.has(Networking.Request.METHODS, opts.method)
, "Networking doesn't provide a `#{opts.method}` method"
if opts.schema?
if utils.isPlainObject(opts.schema)
opts.schema = new Schema opts.schema
assert.instanceOf opts.schema, Schema
if opts.redirect?
if typeof opts.redirect is 'string'
opts.redirect = new Networking.Uri opts.redirect
else
assert.isFunction opts.redirect
if utils.isObject(opts.toHTML)
opts.toHTML = createToHTMLFromObject opts.toHTML
for key, val of opts
@[key] = val
= utils.uid()
= app
||= getRouteName(@)
app.networking.createHandler
method:
uri:
schema:
callback: utils.bindFunctionContext(handleRequest, @)
getRouteName = (route) ->
assert.instanceOf route, Route
uri = route.uri._uri
uri = uri.replace Networking.Uri.NAMES_RE, ''
uri = uri.replace /\*/g, ''
while uri.indexOf('//') isnt -1
uri = uri.replace /\/\//g, '/'
uri = uri.replace /^\//, ''
uri = uri.replace /\/$/, ''
uri
routesCache = Object.create null
pendingRoutes = Object.create null
factoryRoute = do ->
createInstance = (route) ->
r = Object.create route
r.__hash__ = utils.uid()
r.factory?()
r
(route) ->
assert.instanceOf route, Route
id = route.__id__
routesCache[id] ?= []
r = routesCache[id].pop() or createInstance(route)
r = Object.create r
r.request = r.response = null
r.route = r
r._dataPrepared = false
r._destroyViewOnEnd = false
r
destroyRoute = (route) ->
assert.instanceOf route, Route
if lastClientHTMLRoute is route
lastClientHTMLRoute = null
route.response.onSend.disconnect onResponseSent, route
pendingRoutes[route.__hash__] = false
route.destroy?()
if route._dataPrepared
switch route.request.type
when 'text'
route.destroyText?()
when 'json'
route.destroyJSON?()
when 'html'
route.destroyHTML?()
if route._destroyViewOnEnd
route.response.data.destroy()
routesCache[route.__id__].push Object.getPrototypeOf(route)
resolveSyncGetDataFunc = (route) ->
assert.instanceOf route, Route
route.data = route.getData()
resolveAsyncGetDataFuncCallback = (route, err, data) ->
assert.instanceOf route, Route
if err?
if route.response.status is 200
route.response.status = 500
if route._dataPrepared and route.error is err
return false
route.error = err
else
if route._dataPrepared and route.data is data
return false
route.data = data
true
prepareRouteData = (route) ->
assert.instanceOf route, Route
{response} = route
if route.error
log.error "Error in route '#{route.uri}':\n#{route.error}"
respData = response.data
switch route.request.type
when 'text'
data = route.toText()
when 'json'
data = route.toJSON()
when 'html'
data = route.toHTML()
if respData instanceof Document and route._destroyViewOnEnd
respData.destroy()
response.data = null
if not (data instanceof Document) and response.data is respData
data = renderViewFromConfig.call route, data
route._dataPrepared = true
if data?
response.data = data
else if response.data is respData
response.data = ''
onResponseSent = ->
if utils.isNode or .type isnt 'html'
destroyRoute @
if utils.isNode and utils.has(usedTemplates, .data)
.data.destroy()
utils.remove usedTemplates, .data
return
finishRequest = (route) ->
assert.instanceOf route, Route
if route.response.data instanceof Document
Renderer.window.document.node = route.response.data.node
if route.response.pending
route.response.send()
return
callNextIfNeeded = (route, next) ->
unless pendingRoutes[route.__hash__]
if route.response.pending
next()
return true
false
handleRequest = (req, res, next) ->
assert.instanceOf req, Networking.Request
assert.instanceOf res, Networking.Response
assert.isFunction next
route = factoryRoute @
hash = route.__hash__
assert.notOk pendingRoutes[hash]
if utils.isClient
if lastClientHTMLRoute
destroyRoute lastClientHTMLRoute
lastClientHTMLRoute = route
route.request = req
route.response = res
pendingRoutes[hash] = true
res.onSend onResponseSent, route
# init
route.init?()
unless pendingRoutes[hash]
return next()
# redirect
{redirect} = route
if typeof redirect is 'function'
redirect = route.redirect()
unless pendingRoutes[hash]
return next()
if typeof redirect is 'string'
redirect = new Networking.Uri redirect
if redirect instanceof Networking.Uri
res.redirect redirect.toString(req.params)
return
# getData
{getData} = route
fakeAsync = false
if typeof getData is 'function'
if getData.length is 1
route.getData (err, data) ->
fakeAsync = true
if callNextIfNeeded(route, next)
return
unless resolveAsyncGetDataFuncCallback(route, err, data)
return
prepareRouteData route
if callNextIfNeeded(route, next)
return
finishRequest route
else
resolveSyncGetDataFunc route
if callNextIfNeeded(route, next)
return
prepareRouteData route
if callNextIfNeeded(route, next)
return
finishRequest route
else
prepareRouteData route
if callNextIfNeeded(route, next)
return
finishRequest route
if not fakeAsync and callNextIfNeeded(route, next)
return
## *String* Route::method = `'get'`
## *Networking.Uri* Route::uri
## *App* Route::app
## *App.Route* Route::route
## *String* Route::name
## *Schema* Route::schema
## *Any* Route::data
## *Any* Route::error
## *Function* Route::factory()
## *Function* Route::init()
## *Function* Route::getData([*Function* callback])
## *Function* Route::destroy()
## *Function* Route::destroyJSON()
## *Function* Route::destroyText()
## *Function* Route::destroyHTML()
## *Networking.Uri* Route::redirect
Can be also a function. May returns a *Networking.Uri*, any String or `undefined`.
## *Networking.Request* Route::request
## *Networking.Response* Route::response
## *Function* Route::next()
next: ->
assert.ok pendingRoutes[]
destroyRoute @
## *Any* Route::toJSON()
toJSON: ->
if .status < 400
?.toJSON?() or
else
.toJSON?() or
## *String* Route::toText()
toText: ->
if .status < 400
+''
else
+''
## *Document* Route::toHTML()
routeToString = ->
"#{@method} #{@uri}"
getDefaultRouteViewName = ->
path = "views/#{@name}.xhtml"
if app.views[path]
return path
renderViewFromConfig = (opts) ->
viewName = opts?.view or getDefaultRouteViewName.call(@) or 'views/index.xhtml'
tmplName = opts?.template or 'views/template.xhtml'
useName = opts?.use or 'body'
logtime = log.time 'Render'
if viewName isnt tmplName
if tmpl = app.views[tmplName]
tmplView = Route.getTemplateView tmplName
tmplView.use useName, null
else
log.warn "Template view '#{tmplName}' can't be found for route '#{routeToString.call(@)}'"
if view = app.views[viewName]
r = view.render null, @
else
log.warn "View '#{tmplName}' can't be found for route '#{routeToString.call(@)}'"
if tmplView
if r?
r = tmplView.use(useName, r)
else
r = tmplView
if tmplView.context.routes.has(useName)
tmplView.context.routes.pop useName
tmplView.context.routes.set useName, @
= false
else
= true
log.end logtime
r
createToHTMLFromObject = (opts) ->
-> renderViewFromConfig.call @, opts
toHTML: createToHTMLFromObject
view: ''
template: ''
use: ''