UNPKG

@leansdk/leanrc

Version:

LeanRC is a MVC framework for creating graceful applications

694 lines (644 loc) 23.8 kB
(function() { // This file is part of LeanRC. // LeanRC is free software: you can redistribute it and/or modify // it under the terms of the GNU Lesser General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // LeanRC is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Lesser General Public License for more details. // You should have received a copy of the GNU Lesser General Public License // along with LeanRC. If not, see <https://www.gnu.org/licenses/>. // EventEmitter = require 'events' var methods; methods = require('methods'); // pathToRegexp = require 'path-to-regexp' // assert = require 'assert' // Stream = require 'stream' // onFinished = require 'on-finished' /* ```coffee module.exports = (Module)-> class HttpSwitch extends Module::Switch @inheritProtected() @include Module::ArangoSwitchMixin @module Module @public routerName: String, default: 'ApplicationRouter' @public jsonRendererName: String, default: 'JsonRenderer' # or 'ApplicationRenderer' HttpSwitch.initialize() ``` */ module.exports = function(Module) { var APPLICATION_MEDIATOR, APPLICATION_ROUTER, AnyT, AsyncFunctionT, ConfigurableMixin, Context, ContextInterface, DictG, FuncG, HANDLER_RESULT, InterfaceG, ListG, MIGRATIONS, MaybeG, Mediator, NotificationInterface, PointerT, Renderer, RendererInterface, ResourceInterface, StructG, Switch, SwitchInterface, UnionG, _, co, genRandomAlphaNumbers, inflect, isGeneratorFunction, statuses; ({ MIGRATIONS, APPLICATION_ROUTER, APPLICATION_MEDIATOR, HANDLER_RESULT, AnyT, PointerT, AsyncFunctionT, FuncG, ListG, MaybeG, InterfaceG, StructG, DictG, UnionG, SwitchInterface, ContextInterface, RendererInterface, NotificationInterface, ResourceInterface, Mediator, Context, ConfigurableMixin, Renderer, Utils: {_, inflect, co, isGeneratorFunction, genRandomAlphaNumbers, statuses} } = Module.prototype); return Switch = (function() { var Class, decode, ipoHttpServer, ipoRenderers, matches; class Switch extends Mediator {}; Switch.inheritProtected(); Switch.include(ConfigurableMixin); Switch.implements(SwitchInterface); Switch.module(Module); ipoHttpServer = PointerT(Switch.private({ httpServer: Object })); ipoRenderers = PointerT(Switch.private({ renderers: MaybeG(DictG(String, MaybeG(RendererInterface))) })); Switch.public({ middlewares: Array }); Switch.public({ handlers: Array }); Switch.public({ responseFormats: ListG(String) }, { get: function() { return ['json', 'html', 'xml', 'atom', 'text']; } }); Switch.public({ routerName: String }, { default: APPLICATION_ROUTER }); Switch.public({ defaultRenderer: String }, { default: 'json' }); Switch.public(Switch.static({ compose: FuncG([ListG(Function), ListG(MaybeG(ListG(Function)))], AsyncFunctionT) }, { default: function(middlewares, handlers) { if (!_.isArray(middlewares)) { throw new Error('Middleware stack must be an array!'); } if (!_.isArray(handlers)) { throw new Error('Handlers stack must be an array!'); } return co.wrap(function*(context) { var handler, handlerGroup, i, j, k, len, len1, len2, middleware, runned; for (i = 0, len = middlewares.length; i < len; i++) { middleware = middlewares[i]; if (!_.isFunction(middleware)) { throw new Error('Middleware must be composed of functions!'); } yield middleware(context); } runned = false; for (j = 0, len1 = handlers.length; j < len1; j++) { handlerGroup = handlers[j]; if (handlerGroup == null) { continue; } for (k = 0, len2 = handlerGroup.length; k < len2; k++) { handler = handlerGroup[k]; if (!_.isFunction(handler)) { throw new Error('Handler must be composed of functions!'); } if ((yield handler(context))) { runned = true; break; } } if (runned) { break; } } }); } })); // from https://github.com/koajs/route/blob/master/index.js ############### decode = FuncG([MaybeG(String)], MaybeG(String))(function(val) { // чистая функция if (val) { return decodeURIComponent(val); } }); matches = FuncG([ContextInterface, String], Boolean)(function(ctx, method) { if (!method) { return true; } if (ctx.method === method) { return true; } if (method === 'GET' && ctx.method === 'HEAD') { return true; } return false; }); Switch.public(Switch.static({ createMethod: FuncG([MaybeG(String)]) }, { default: function(method) { var originMethodName; originMethodName = method; if (method) { method = method.toUpperCase(); } else { originMethodName = 'all'; } this.public({ [`${originMethodName}`]: FuncG([String, Function]) }, { default: function(path, routeFunc) { var DEBUG, ERROR, LEVELS, SEND_TO_LOG, facade, keys, pathToRegexp, re, self; if (!routeFunc) { throw new Error('handler is required'); } ({facade} = this); ({ERROR, DEBUG, LEVELS, SEND_TO_LOG} = Module.prototype.LogMessage); self = this; keys = []; pathToRegexp = require('path-to-regexp'); re = pathToRegexp(path, keys); facade.sendNotification(SEND_TO_LOG, `${method != null ? method : 'ALL'} ${path} -> ${re}`, LEVELS[DEBUG]); this.use(keys.length, co.wrap(function*(ctx) { var m, pathParams; if (!matches(ctx, method)) { return; } m = re.exec(ctx.path); if (m) { pathParams = m.slice(1).map(decode).reduce(function(prev, item, index) { prev[keys[index].name] = item; return prev; }, {}); ctx.routePath = path; facade.sendNotification(SEND_TO_LOG, `${ctx.method} ${path} matches ${ctx.path} ${JSON.stringify(pathParams)}`, LEVELS[DEBUG]); ctx.pathParams = pathParams; return (yield routeFunc.call(self, ctx)); } })); } }); } })); // это надо будет заиспользовать когда решится вопрос "как подрубить свайгер" //@defineSwaggerEndpoint voEndpoint Class = Switch; methods.forEach(function(method) { // console.log 'SWITCH:', @ return Class.createMethod(method); }); Switch.public({ del: Function }, { default: function(...args) { return this.delete(...args); } }); Switch.createMethod(); // create @public all:... //######################################################################### // @public jsonRendererName: String // @public htmlRendererName: String // @public xmlRendererName: String // @public atomRendererName: String Switch.public({ listNotificationInterests: FuncG([], Array) }, { default: function() { return [HANDLER_RESULT]; } }); Switch.public({ handleNotification: FuncG(NotificationInterface) }, { default: function(aoNotification) { var voBody, vsName, vsType; vsName = aoNotification.getName(); voBody = aoNotification.getBody(); vsType = aoNotification.getType(); switch (vsName) { case HANDLER_RESULT: this.getViewComponent().emit(vsType, voBody); } } }); Switch.public({ onRegister: Function }, { default: function() { var EventEmitter, voEmitter; EventEmitter = require('events'); voEmitter = new EventEmitter(); if (voEmitter.listeners('error').length === 0) { voEmitter.on('error', this.onerror.bind(this)); } this.setViewComponent(voEmitter); this.defineRoutes(); this.serverListen(); } }); Switch.public({ onRemove: Function }, { default: function() { var voEmitter; voEmitter = this.getViewComponent(); voEmitter.eventNames().forEach(function(eventName) { return voEmitter.removeAllListeners(eventName); }); this[ipoHttpServer].close(); } }); Switch.public({ serverListen: Function }, { default: function() { var facade, http, port, ref, ref1; port = (ref = typeof process !== "undefined" && process !== null ? (ref1 = process.env) != null ? ref1.PORT : void 0 : void 0) != null ? ref : this.configs.port; ({facade} = this); http = require('http'); this[ipoHttpServer] = http.createServer(this.callback()); this[ipoHttpServer].listen(port, function() { var DEBUG, ERROR, LEVELS, SEND_TO_LOG; // console.log "listening on port #{port}" ({ERROR, DEBUG, LEVELS, SEND_TO_LOG} = Module.prototype.LogMessage); return facade.sendNotification(SEND_TO_LOG, `listening on port ${port}`, LEVELS[DEBUG]); }); } }); Switch.public({ use: FuncG([UnionG(Number, Function), MaybeG(Function)], SwitchInterface) }, { default: function(index, middleware) { var DEBUG, ERROR, LEVELS, SEND_TO_LOG, base, middlewareName, oldName, ref, ref1; if (middleware == null) { middleware = index; index = null; } if (!_.isFunction(middleware)) { throw new Error('middleware or handler must be a function!'); } if (isGeneratorFunction(middleware)) { ({ name: oldName } = middleware); middleware = co.wrap(middleware); middleware._name = oldName; } middlewareName = (ref = (ref1 = middleware._name) != null ? ref1 : middleware.name) != null ? ref : '-'; ({ERROR, DEBUG, LEVELS, SEND_TO_LOG} = Module.prototype.LogMessage); this.sendNotification(SEND_TO_LOG, `use ${middlewareName}`, LEVELS[DEBUG]); if (index != null) { if ((base = this.handlers)[index] == null) { base[index] = []; } this.handlers[index].push(middleware); } else { this.middlewares.push(middleware); } return this; } }); Switch.public({ callback: FuncG([], AsyncFunctionT) }, { default: function() { var fn, handleRequest, onFinished, self; fn = this.constructor.compose(this.middlewares, this.handlers); self = this; onFinished = require('on-finished'); handleRequest = co.wrap(function*(req, res) { var DEBUG, ERROR, LEVELS, SEND_TO_LOG, err, reqLength, resLength, t1, time, voContext; t1 = Date.now(); ({ERROR, DEBUG, LEVELS, SEND_TO_LOG} = Module.prototype.LogMessage); self.sendNotification(SEND_TO_LOG, '>>>>>> START REQUEST HANDLING', LEVELS[DEBUG]); res.statusCode = 404; voContext = Context.new(req, res, self); try { yield fn(voContext); self.respond(voContext); } catch (error1) { err = error1; voContext.onerror(err); } onFinished(res, function(err) { voContext.onerror(err); }); self.sendNotification(SEND_TO_LOG, '>>>>>> END REQUEST HANDLING', LEVELS[DEBUG]); reqLength = voContext.request.length; resLength = voContext.response.length; time = Date.now() - t1; yield self.handleStatistics(reqLength, resLength, time, voContext); }); return handleRequest; } }); // NOTE: пустая функция, которую вызываем из callback и передаем в нее длину реквеста, длину респонза, время выполнения, и контекст, чтобы потом в отдельном миксине можно было определить тело этого метода, т.е. как реализовывать сохранение (реагировать) этой статистики. Switch.public(Switch.async({ handleStatistics: FuncG([Number, Number, Number, ContextInterface]) }, { default: function*(reqLength, resLength, time, aoContext) { var DEBUG, LEVELS, SEND_TO_LOG; ({DEBUG, LEVELS, SEND_TO_LOG} = Module.prototype.LogMessage); this.sendNotification(SEND_TO_LOG, `REQUEST LENGTH ${reqLength} byte RESPONSE LENGTH ${resLength} byte HANDLED BY ${time} ms`, LEVELS[DEBUG]); } })); // Default error handler Switch.public({ onerror: FuncG(Error) }, { default: function(err) { var DEBUG, ERROR, LEVELS, SEND_TO_LOG, assert, msg, ref; assert = require('assert'); assert(_.isError(err), `non-error thrown: ${err}`); if (404 === err.status || err.expose) { return; } if (this.configs.silent) { return; } msg = (ref = err.stack) != null ? ref : String(err); ({ERROR, DEBUG, LEVELS, SEND_TO_LOG} = Module.prototype.LogMessage); this.sendNotification(SEND_TO_LOG, msg.replace(/^/gm, ' '), LEVELS[ERROR]); } }); Switch.public({ respond: FuncG(ContextInterface) }, { default: function(ctx) { var body, code, ref; if (ctx.respond === false) { return; } if (!ctx.writable) { return; } body = ctx.body; code = ctx.status; if (statuses.empty[code]) { ctx.body = null; ctx.res.end(); return; } if ('HEAD' === ctx.method) { if (!ctx.res.headersSent && _.isObjectLike(body)) { ctx.length = Buffer.byteLength(JSON.stringify(body)); } ctx.res.end(); return; } if (body == null) { body = (ref = ctx.message) != null ? ref : String(code); if (!ctx.res.headersSent) { ctx.type = 'text'; ctx.length = Buffer.byteLength(body); } ctx.res.end(body); return; } if (_.isBuffer(body) || _.isString(body)) { ctx.res.end(body); return; } if (body instanceof require('stream')) { body.pipe(ctx.res); return; } body = JSON.stringify(body != null ? body : null); if (!ctx.res.headersSent) { ctx.length = Buffer.byteLength(body); } ctx.res.end(body); } }); Switch.public({ rendererFor: FuncG(String, RendererInterface) }, { default: function(asFormat) { var base; if (this[ipoRenderers] == null) { this[ipoRenderers] = {}; } if ((base = this[ipoRenderers])[asFormat] == null) { base[asFormat] = ((asFormat) => { var voRenderer; voRenderer = this[`${asFormat}RendererName`] != null ? this.facade.retrieveProxy(this[`${asFormat}RendererName`]) : void 0; if (voRenderer == null) { voRenderer = Renderer.new(); } return voRenderer; })(asFormat); } return this[ipoRenderers][asFormat]; } }); Switch.public(Switch.async({ sendHttpResponse: FuncG([ ContextInterface, MaybeG(AnyT), ResourceInterface, InterfaceG({ method: String, path: String, resource: String, action: String, tag: String, template: String, keyName: MaybeG(String), entityName: String, recordName: MaybeG(String) }) ]) }, { default: function*(ctx, aoData, resource, opts) { var ref, voRendered, voRenderer, vsFormat; if (opts.action === 'create') { ctx.status = 201; } // unless ctx.headers?.accept? // yield return // switch (vsFormat = ctx.accepts @responseFormats) // when no // else // if @["#{vsFormat}RendererName"]? // voRenderer = @rendererFor vsFormat // voRendered = yield voRenderer // .render ctx, aoData, resource, opts // ctx.body = voRendered // yield return if (((ref = ctx.headers) != null ? ref.accept : void 0) != null) { switch ((vsFormat = ctx.accepts(this.responseFormats))) { case false: break; default: if (this[`${vsFormat}RendererName`] != null) { voRenderer = this.rendererFor(vsFormat); } } } else { if (this[`${this.defaultRenderer}RendererName`] != null) { voRenderer = this.rendererFor(this.defaultRenderer); } } if (voRenderer != null) { voRendered = (yield voRenderer.render(ctx, aoData, resource, opts)); ctx.body = voRendered; } } })); Switch.public({ defineRoutes: Function }, { default: function() { var ref, voRouter; voRouter = this.facade.retrieveProxy((ref = this.routerName) != null ? ref : APPLICATION_ROUTER); voRouter.routes.forEach((aoRoute) => { return this.createNativeRoute(aoRoute); }); } }); Switch.public({ sender: FuncG([ String, StructG({ context: ContextInterface, reverse: String }), InterfaceG({ method: String, path: String, resource: String, action: String, tag: String, template: String, keyName: MaybeG(String), entityName: String, recordName: MaybeG(String) }) ]) }, { default: function(resourceName, aoMessage, {method, path, resource, action}) { this.sendNotification(resourceName, aoMessage, action); } }); // @public defineSwaggerEndpoint: Function, // default: (aoSwaggerEndpoint, resourceName, action)-> // voGateway = @facade.retrieveProxy "#{resourceName}Gateway" // { // tags // headers // pathParams // queryParams // payload // responses // errors // title // synopsis // isDeprecated // } = voGateway.swaggerDefinitionFor action // tags?.forEach (tag)-> // aoSwaggerEndpoint.tag tag // headers?.forEach ({name, schema, description})-> // aoSwaggerEndpoint.header name, schema, description // pathParams?.forEach ({name, schema, description})-> // aoSwaggerEndpoint.pathParam name, schema, description // queryParams?.forEach ({name, schema, description})-> // aoSwaggerEndpoint.queryParam name, schema, description // if payload? // aoSwaggerEndpoint.body payload.schema, payload.mimes, payload.description // responses?.forEach ({status, schema, mimes, description})-> // aoSwaggerEndpoint.response status, schema, mimes, description // errors?.forEach ({status, description})-> // aoSwaggerEndpoint.error status, description // aoSwaggerEndpoint.summary title if title? // aoSwaggerEndpoint.description synopsis if synopsis? // aoSwaggerEndpoint.deprecated isDeprecated if isDeprecated? // return Switch.public({ createNativeRoute: FuncG([ InterfaceG({ method: String, path: String, resource: String, action: String, tag: String, template: String, keyName: MaybeG(String), entityName: String, recordName: MaybeG(String) }) ]) }, { default: function(opts) { var method, path, resourceName, self; ({method, path} = opts); resourceName = inflect.camelize(inflect.underscore(`${opts.resource.replace(/[\/]/g, '_')}Resource`)); self = this; if (typeof this[method] === "function") { this[method](path, co.wrap(function*(context) { yield Module.prototype.Promise.new(function(resolve, reject) { var err, reverse; try { reverse = genRandomAlphaNumbers(32); self.getViewComponent().once(reverse, co.wrap(function*({error, result, resource}) { var err; if (error != null) { console.log('>>>>>> ERROR AFTER RESOURCE', error); reject(error); return; } try { yield self.sendHttpResponse(context, result, resource, opts); resolve(); } catch (error1) { err = error1; reject(err); } })); self.sender(resourceName, {context, reverse}, opts); } catch (error1) { err = error1; reject(err); } }); return true; })); } } }); Switch.public({ init: FuncG([MaybeG(String), MaybeG(AnyT)]) }, { default: function(...args) { this.super(...args); this[ipoRenderers] = {}; this.middlewares = []; this.handlers = []; } }); Switch.initialize(); return Switch; }).call(this); }; }).call(this);