// 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