// Copyright (c) 2014 Quildreen Motta // // Permission is hereby granted, free of charge, to any person // obtaining a copy of this software and associated documentation files // (the "Software"), to deal in the Software without restriction, // including without limitation the rights to use, copy, modify, merge, // publish, distribute, sublicense, and/or sell copies of the Software, // and to permit persons to whom the Software is furnished to do so, // subject to the following conditions: // // The above copyright notice and this permission notice shall be // included in all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. /** * Monadic wrapper over Express web framework. * * @module express */ // -- Dependencies ----------------------------------------------------- var adt = require('adt-simple'); var curry = require('core.lambda').curry; var compose = require('core.lambda').compose; var Future = require('data.future'); var extend = require('xtend'); var methods = require('methods'); var destructiveExtend = require('xtend/mutable'); // -- Data structures -------------------------------------------------- /** * Represents a server's configuration. * * Servers are made up of several components, which are stacked on top * of each other in the order they're given. Components that are * installed first have precedence, and are executed first whenever * a handler for a particular route is called. * * @class * @summary * | Setting: { name: String, value: Any } * | Plugin: { path: String, handler: (Request, ExpressRes, (Void → Void) → Void) } * | Route: { method: String, spec: String|RegExp, handler: (Request → Future[Error, Response]) } * | Engine: { extension: String, engine: (Path, Object, (Error, String → Void) → Void) } */ var Component = function () { function Component$2() { } function Setting$2(name, value) { if (!(this instanceof Setting$2)) { return new Setting$2(name, value); } if (typeof name === 'string' || Object.prototype.toString.call(name) === '[object String]') { this.name = name; } else { throw new TypeError('Unexpected type for field: Component.Setting.name'); } this.value = value; } Setting$2.prototype = new Component$2(); Setting$2.prototype.constructor = Setting$2; function Plugin$2(path, handler) { if (!(this instanceof Plugin$2)) { return new Plugin$2(path, handler); } if (typeof path === 'string' || Object.prototype.toString.call(path) === '[object String]') { this.path = path; } else { throw new TypeError('Unexpected type for field: Component.Plugin.path'); } if (Object.prototype.toString.call(handler) === '[object Function]') { this.handler = handler; } else { throw new TypeError('Unexpected type for field: Component.Plugin.handler'); } } Plugin$2.prototype = new Component$2(); Plugin$2.prototype.constructor = Plugin$2; function Route$2(method, spec, handler) { if (!(this instanceof Route$2)) { return new Route$2(method, spec, handler); } if (typeof method === 'string' || Object.prototype.toString.call(method) === '[object String]') { this.method = method; } else { throw new TypeError('Unexpected type for field: Component.Route.method'); } this.spec = spec; if (Object.prototype.toString.call(handler) === '[object Function]') { this.handler = handler; } else { throw new TypeError('Unexpected type for field: Component.Route.handler'); } } Route$2.prototype = new Component$2(); Route$2.prototype.constructor = Route$2; function Engine$2(extension, engine) { if (!(this instanceof Engine$2)) { return new Engine$2(extension, engine); } if (typeof extension === 'string' || Object.prototype.toString.call(extension) === '[object String]') { this.extension = extension; } else { throw new TypeError('Unexpected type for field: Component.Engine.extension'); } if (Object.prototype.toString.call(engine) === '[object Function]') { this.engine = engine; } else { throw new TypeError('Unexpected type for field: Component.Engine.engine'); } } Engine$2.prototype = new Component$2(); Engine$2.prototype.constructor = Engine$2; function Locals$2(value) { if (!(this instanceof Locals$2)) { return new Locals$2(value); } this.value = value; } Locals$2.prototype = new Component$2(); Locals$2.prototype.constructor = Locals$2; var derived = adt.Cata.derive(adt.Base.derive({ name: 'Component', constructor: Component$2, prototype: Component$2.prototype, variants: [ { name: 'Setting', constructor: Setting$2, prototype: Setting$2.prototype, fields: [ 'name', 'value' ] }, { name: 'Plugin', constructor: Plugin$2, prototype: Plugin$2.prototype, fields: [ 'path', 'handler' ] }, { name: 'Route', constructor: Route$2, prototype: Route$2.prototype, fields: [ 'method', 'spec', 'handler' ] }, { name: 'Engine', constructor: Engine$2, prototype: Engine$2.prototype, fields: [ 'extension', 'engine' ] }, { name: 'Locals', constructor: Locals$2, prototype: Locals$2.prototype, fields: ['value'] } ] })); Component$2.Setting = derived.variants[0].constructor; Component$2.Plugin = derived.variants[1].constructor; Component$2.Route = derived.variants[2].constructor; Component$2.Engine = derived.variants[3].constructor; Component$2.Locals = derived.variants[4].constructor; return Component$2; }(); var Setting = Component.Setting; var Plugin = Component.Plugin; var Route = Component.Route; var Engine = Component.Engine; var Locals = Component.Locals; /** * Represents the response from a route handler. * * @class * @summary * | Redirect: { url: URL } * | Render: { view: Path, options: Object } * | Send: { status: Number, headers: Object, body: Content } */ var Response = function () { function Response$2() { } function Redirect$2(url) { if (!(this instanceof Redirect$2)) { return new Redirect$2(url); } this.url = url; } Redirect$2.prototype = new Response$2(); Redirect$2.prototype.constructor = Redirect$2; function Render$2(view, options) { if (!(this instanceof Render$2)) { return new Render$2(view, options); } if (typeof view === 'string' || Object.prototype.toString.call(view) === '[object String]') { this.view = view; } else { throw new TypeError('Unexpected type for field: Response.Render.view'); } this.options = options; } Render$2.prototype = new Response$2(); Render$2.prototype.constructor = Render$2; function Send$2(status, headers, body) { if (!(this instanceof Send$2)) { return new Send$2(status, headers, body); } if (typeof status === 'number' || Object.prototype.toString.call(status) === '[object Number]') { this.status = status; } else { throw new TypeError('Unexpected type for field: Response.Send.status'); } this.headers = headers; if (body instanceof Content) { this.body = body; } else { throw new TypeError('Unexpected type for field: Response.Send.body'); } } Send$2.prototype = new Response$2(); Send$2.prototype.constructor = Send$2; var derived = adt.Cata.derive(adt.Base.derive({ name: 'Response', constructor: Response$2, prototype: Response$2.prototype, variants: [ { name: 'Redirect', constructor: Redirect$2, prototype: Redirect$2.prototype, fields: ['url'] }, { name: 'Render', constructor: Render$2, prototype: Render$2.prototype, fields: [ 'view', 'options' ] }, { name: 'Send', constructor: Send$2, prototype: Send$2.prototype, fields: [ 'status', 'headers', 'body' ] } ] })); Response$2.Redirect = derived.variants[0].constructor; Response$2.Render = derived.variants[1].constructor; Response$2.Send = derived.variants[2].constructor; return Response$2; }(); var Redirect = Response.Redirect; var Render = Response.Render; var Send = Response.Send; /** * Represents the body of a response. * * @class * @summary * | Buffer: { value: Buffer } * | Text: { value: String } * | Value: { value: Any } */ var Content = function () { function Content$2() { } function Buffer$2(value) { if (!(this instanceof Buffer$2)) { return new Buffer$2(value); } this.value = value; } Buffer$2.prototype = new Content$2(); Buffer$2.prototype.constructor = Buffer$2; function Text$2(value) { if (!(this instanceof Text$2)) { return new Text$2(value); } this.value = value; } Text$2.prototype = new Content$2(); Text$2.prototype.constructor = Text$2; function Value$2(value) { if (!(this instanceof Value$2)) { return new Value$2(value); } this.value = value; } Value$2.prototype = new Content$2(); Value$2.prototype.constructor = Value$2; var derived = adt.Cata.derive(adt.Base.derive({ name: 'Content', constructor: Content$2, prototype: Content$2.prototype, variants: [ { name: 'Buffer', constructor: Buffer$2, prototype: Buffer$2.prototype, fields: ['value'] }, { name: 'Text', constructor: Text$2, prototype: Text$2.prototype, fields: ['value'] }, { name: 'Value', constructor: Value$2, prototype: Value$2.prototype, fields: ['value'] } ] })); Content$2.Buffer = derived.variants[0].constructor; Content$2.Text = derived.variants[1].constructor; Content$2.Value = derived.variants[2].constructor; return Content$2; }(); var Buffer = Content.Buffer; var Text = Content.Text; var Value = Content.Value; // -- Helpers ---------------------------------------------------------- /** * Applies a configuration to an express application. * * @private * @summary App, Component → App */ function configure(app, route) { (function (a0) { var r13 = Setting.unapply(a0); if (r13 != null && r13.length === 2) { var r14 = r13[0]; var r15 = r13[1]; var name = r14, value = r15; return app.set(name, value); } var r16 = Plugin.unapply(a0); if (r16 != null && r16.length === 2) { var r17 = r16[0]; var r18 = r16[1]; var mountPoint = r17, handler = r18; return app.use(mountPoint, handler); } var r19 = Engine.unapply(a0); if (r19 != null && r19.length === 2) { var r20 = r19[0]; var r21 = r19[1]; var extension = r20, engine = r21; return app.engine(extension, engine); } var r22 = Locals.unapply(a0); if (r22 != null && r22.length === 1) { var r23 = r22[0]; var object = r23; return destructiveExtend(app.locals, object); } var r24 = Route.unapply(a0); if (r24 != null && r24.length === 3) { var r25 = r24[0]; var r26 = r24[1]; var r27 = r24[2]; var method = r25, spec = r26, handler = r27; return handleRequest(app, method, spec, handler); } throw new TypeError('No match'); }.call(this, route)); return app; } /** * Setups a request handler for a particular route-spec. * * @private * @summary App, Method, RouteSpec, (Request → Future[Error, Repsonse]) → App * : where RouteSpec = RegExp | String */ function handleRequest(app, method, spec, handler) { app[method.toLowerCase()](spec, function (req, res) { handler(req).fork(handleError(req, res), sendResponse(req, res)); }); return app; } /** * Handles an error while handling the response. * * @private * @summary Request, ExpressResponse → Error | Response → Void */ function handleError(req, res) { return function (error) { console.log(error.toString()); if (error instanceof Response) sendResponse(req, res)(error); else res.send(500, error); }; } /** * Handles a response from a handler. * * @private * @summary Request, ExpressResponse → Response → Void */ function sendResponse(req, res) { return function (a0) { var r13 = Redirect.unapply(a0); if (r13 != null && r13.length === 1) { var r14 = r13[0]; var url = r14; return res.redirect(url.toString()); } var r15 = Render.unapply(a0); if (r15 != null && r15.length === 2) { var r16 = r15[0]; var r17 = r15[1]; var view = r16, options = r17; return res.render(view, options); } var r18 = Send.unapply(a0); if (r18 != null && r18.length === 3) { var r19 = r18[0]; var r20 = r18[1]; var r21 = r18[2]; var status = r19, headers = r20, body = r21; return _send(res, status, headers, body); } throw new TypeError('No match'); }; } /** * Sends a response. * * @private * @summary ExpressResponse, Int, Object, Body → Void */ function _send(res, status, headers, body) { res.set(headers || {}); body.cata({ Text: function (v) { res.send(status, String(v)); }, Buffer: function (v) { res.send(status, v); }, Value: function (v) { res.send(status, v); } }); } /** * Returns a wrapper over Node's `Server` objects that provides * monadic actions. * * @private * @summary Server → { close: Void → Future[Error, Void] } */ function wrapServer(server) { return { close: function () { return new Future(function (reject, resolve) { server.close(function (error) { if (error) reject(error); else resolve(); }); }); } }; } // -- Public API ------------------------------------------------------- module.exports = function (express) { var exports = {}; /** * Constructs a configuration for an Express application. * * @method * @summary Name → Value → Component */ exports.set = curry(2, Setting); /** * Shor-hand for [`set(name, true)`](#set) * * @method * @summary Name → Component */ exports.enable = enable; function enable(name) { return Setting(name, true); } /** * Shor-hand for [`set(name, false)`](#set) * * @method * @summary Name → Component */ exports.disable = disable; function disable(name) { return Setting(name, false); } /** * Constructs an engine configuration for an Express application. * * @method * @summary String → (Path, Object, (Error, String → Void) → Void) */ exports.engine = curry(2, Engine); /** * Constructs a locals configuration for an Express application. * * @method * @summary Object → Component */ exports.locals = Locals; /** * Constructs a Plugin configuration for an Express application. * * @method * @summary Path → (Request, ExpressResponse, (Any → Void) → Void) → Component */ exports.plugin = curry(2, Plugin); /** * Constructs a Route configuration for an Express application. * * This module also provides short-hands for HTTP verbs supported by Node.js, * such that you can conveniently use `VERB(spec, handler)` instead of * `route(VERB, spec, handler)`. * * @method * @summary Method → RouteSpec → (Request → Future[Error, Response]) → Component */ var route = exports.route = curry(3, Route); /** * Short-hand for [`route('all')(...)`](#route). * * @method * @summary RouteSpec → (Request → Future[Error, Response]) → Component */ exports.all = route('all'); /** * Short-hand for [`route('delete')(...)`](#route). * * @method * @summary RouteSpec → (Request → Future[Error, Response]) → Component */ exports.remove = route('delete'); /** * Wraps a route handler. * * @method * @summary * ((Request → Future[Error, Response]) → (Request → Future[Error, Response])) * → Component * → Component */ exports.wrap = curry(2, wrap); function wrap(wrapper, route$2) { return function (a0) { var r13 = Route.unapply(a0); if (r13 != null && r13.length === 3) { var r14 = r13[0]; var r15 = r13[1]; var r16 = r13[2]; var method = r14, spec = r15, handler = r16; return Route(method, spec, wrapper(handler)); } throw new TypeError('No match'); }.call(this, route$2); } /** * Binds an Express server to a particular port. * * @method * @summary Int → App → Future[Error, Server] */ exports.listen = curry(2, listen); function listen(port, server) { return new Future(function (reject, resolve) { var s = server.listen(port, function (error) { if (error) reject(error); else delayedResolve(this.address()); }); function delayedResolve(addr) { setTimeout(function () { resolve(extend(wrapServer(s), { address: addr })); }); } }); } /** * Constructs an Express server from a list of configurations. * * @method * @summary [Component] → Server */ exports.create = create; function create(routes) { return routes.reduce(configure, express()); } /** * Constructs a JSON response. * * @method * @summary Object → Response */ exports.json = json; function json(x) { return Send(200, { 'Content-Type': 'application/json' }, Value(x)); } /** * Constructs a String body for a response. * * @method * @summary String → Content */ exports.text = text; function text(a) { return Text(a); } /** * Constructs a JS value body for a response. * * @method * @summary Any → Content */ exports.value = value; function value(a) { return Value(a); } /** * Constructs a Buffer body for a response. * * @method * @summary Buffer → Content */ exports.buffer = buffer; function buffer(a) { return Buffer(a); } /** * Constructs a response for a handler. * * @method * @summary Int → Object → Content → Response */ var send = exports.send = curry(3, Send); /** * Short-hand for [`send(200, { 'Content-Type': 'text/html' }, text(x))`](#send) * * @method * @summary String → Response */ exports.success = compose(send(200, { 'Content-Type': 'text/html' }), text); /** * Short-hand for [`send(404, { 'Content-Type': 'text/html' }, text(x))`](#send) * * @method * @summary String → Response */ exports.notFound = compose(send(404, { 'Content-Type': 'text/html' }), text); /** * Short-hand for [`send(500, { 'Content-Type': 'text/html' }, text(x))`](#send) * * @method * @summary String → Response */ exports.fail = compose(send(500, { 'Content-Type': 'text/html' }), text); /** * Constructs a redirect response for a handler. * * @method * @summary URL → Response */ exports.redirect = Redirect; /** * Constructs a render response for a handler. * * @method * @summary String → Object → Response */ exports.render = curry(2, Render); // -- Exports -------------------------------------------------------- exports.Component = Component; exports.Response = Response; exports.Content = Content; methods.forEach(function (m) { exports[m] = route(m); }); return exports; }; //# sourceMappingURL=index.js.map