neft
Version:
Universal Platform
441 lines (353 loc) • 14.1 kB
text/coffeescript
# Route
'use strict'
utils = require 'src/utils'
signal = require 'src/signal'
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'
IS_NODE = process.env.NEFT_NODE
IS_CLIENT = not IS_NODE
module.exports = (app) -> class Route
if IS_NODE
usedTemplates = []
else
templates = Object.create null
= null
= signal.create()
## *Document* Route.getTemplateComponent(*String* componentName)
= do ->
if IS_NODE
(name) ->
scope = app: app, routes: new Dict
tmpl = app.components[name].render null, scope
usedTemplates.push tmpl
tmpl
else
(name) ->
scope = app: app, routes: new Dict
templates[name] ?= app.components[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._destroyComponentOnEnd = false
r
destroyRoute = (route) ->
assert.instanceOf route, Route
route.response.onSend.disconnect onResponseSent, route
delete pendingRoutes[route.__hash__]
route.destroy?()
if route._dataPrepared
switch route.request.type
when 'text'
route.destroyText?()
when 'json'
route.destroyJSON?()
when 'html'
route.destroyHTML?()
if route._destroyComponentOnEnd
route.response.data.destroy()
routesCache[route.__id__].push Object.getPrototypeOf(route)
if Route.lastClientRoute is route
Route.lastClientRoute = null
Route.onLastClientRouteChange.emit route
return
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._destroyComponentOnEnd
respData.destroy()
response.data = null
if not (data instanceof Document) and response.data is respData
data = renderComponentFromConfig.call route, data
route._dataPrepared = true
if data?
response.data = data
else if response.data is respData
response.data = ''
onResponseSent = ->
if IS_NODE or .type isnt 'html'
destroyRoute @
if IS_NODE and utils.has(usedTemplates, .data)
.data.destroy()
utils.remove usedTemplates, .data
return
finishRequest = (route) ->
assert.instanceOf route, Route
if IS_CLIENT and route.response.data instanceof Document
app.windowItem.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 IS_CLIENT
if Route.lastClientRoute
destroyRoute Route.lastClientRoute
{lastClientRoute} = Route
Route.lastClientRoute = route
Route.onLastClientRouteChange.emit lastClientRoute
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}"
getDefaultRouteComponentName = ->
path = "components/#{@name}.xhtml"
if app.components[path]
return path
renderComponentFromConfig = (opts) ->
componentName = opts?.component or
getDefaultRouteComponentName.call(@) or
'components/index.xhtml'
tmplName = opts?.template or componentName
useName = opts?.use or 'body'
logtime = log.time 'Render'
if componentName isnt tmplName
if tmpl = app.components[tmplName]
tmplComponent = Route.getTemplateComponent tmplName
tmplComponent.use useName, null
else
log.warn """
Template component '#{tmplName}' can't be found \
for route '#{routeToString.call(@)}'
"""
if component = app.components[componentName]
r = component.render null, @
else
log.warn "Component '#{tmplName}' can't be found for route '#{routeToString.call(@)}'"
if tmplComponent
if r?
r = tmplComponent.use(useName, r)
else
r = tmplComponent
if tmplComponent.context.routes.has(useName)
tmplComponent.context.routes.pop useName
tmplComponent.context.routes.set useName, @
= false
else
= true
log.end logtime
r
createToHTMLFromObject = (opts) ->
-> renderComponentFromConfig.call @, opts
toHTML: createToHTMLFromObject
component: ''
template: ''
use: ''