├── .gitignore ├── .npmignore ├── .travis.yml ├── CHANGES.md ├── Jakefile ├── README.md ├── benchmarks ├── alternative-routing │ ├── alt-router.js │ └── app.js ├── hello-world │ └── hello-world.js ├── mustache-layout │ └── app.js ├── route-generator.js ├── run └── views │ ├── index.html │ └── layout.html ├── cli └── templates │ ├── app.js.template │ └── package.json.template ├── docs ├── app.md ├── bogart.md ├── index.md ├── routing.md └── view-engine.md ├── engines └── node │ └── lib │ └── fileWatcher.js ├── examples ├── body-adapter.js ├── config.js ├── cors.js ├── facebook-authentication │ ├── app.js │ └── public │ │ ├── favicon.ico │ │ └── login-with-facebook.png ├── flash-test.js ├── gzip-response.js ├── hello-world.js ├── multipart-form │ └── app.js ├── mustache-layout │ ├── app.js │ ├── package.json │ └── views │ │ ├── index.html │ │ └── layout.html ├── pipe-file.js ├── proxy.js ├── session.js ├── share-javascript │ ├── app.js │ └── views │ │ └── index.html ├── static-server │ ├── app.js │ ├── package.json │ └── public │ │ └── images │ │ └── ninja-cat.jpg └── task-list │ ├── app.js │ └── templates │ ├── index.html │ └── layout.html ├── lib ├── bogart.js ├── flash │ ├── flashCookieDataProvider.js │ └── flashIdCookieProvider.js ├── forEachStream.js ├── forEachable.js ├── fsp.js ├── middleware.js ├── middleware │ ├── bodyAdapter.js │ ├── cascade.js │ ├── directory.js │ ├── error.js │ ├── facebook.js │ ├── flash.js │ ├── google.js │ ├── gzip.js │ ├── methodOverride.js │ ├── oauth.js │ ├── oauth2.js │ ├── parseForm.js │ ├── parseJson.js │ ├── parted.js │ ├── renderView.js │ ├── session │ │ ├── cookieDataProvider.js │ │ ├── idProvider.js │ │ └── session.js │ ├── stringReturnAdapter.js │ ├── twitter.js │ └── validateResponse.js ├── mimetypes.js ├── q.js ├── request.js ├── router.js ├── security.js ├── stream.js ├── util.js └── view.js ├── package.json └── spec ├── appSpec.coffee ├── fixtures ├── chrome.part ├── greeting.mustache ├── index.mustache ├── partial-test.mustache ├── test.jpg └── text.txt ├── helpers └── JsgiRequestHelper.coffee ├── middleware ├── facebookSpec.coffee └── session │ └── cookieDataProviderSpec.coffee ├── middlewareSpec.coffee ├── requestSpec.coffee ├── responseBuilderSpec.coffee ├── responseHelperSpec.coffee ├── routerSpec.coffee ├── streamSpec.coffee ├── utilSpec.coffee └── viewSpec.coffee /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | downloaded-modules/ 3 | results/ 4 | hello-world.txt 5 | *.dat 6 | forEachableToFileStream.txt 7 | *.sublime-project 8 | *.sublime-workspace 9 | lib-cov 10 | node_modules 11 | 12 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | *.sublime-* 2 | forEachableStream.txt 3 | npm-debug.log 4 | hello-world.dat 5 | hello-world.txt 6 | examples/ 7 | _site/ 8 | spec/ 9 | 10 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 0.10 -------------------------------------------------------------------------------- /CHANGES.md: -------------------------------------------------------------------------------- 1 | ## v0.5.9 2 | 3 | * Ignores `examples` directory in npm releases. 4 | 5 | ## v0.5.8 6 | 7 | * Uses default `directory` middleware configuration if `config` is provided but no 8 | no value for `directory` is present in call to `bogart.batteries`. 9 | 10 | ## v0.5.7 11 | 12 | * Updates Q dependency to version 0.9.6. This version of Q has many performance enhancements. 13 | 14 | ## v0.5.6 15 | 16 | * Updates static server example to use `bogart.app`. 17 | * Switches Travis configuration to only run tests in node v0.10.x. 18 | * Updates stream specs for node v0.10.x style streams. 19 | * Fixes a bug with `bogart.pump` resolving its promise at wrong time. 20 | 21 | ## v0.5.5 22 | 23 | * Falls back to `process.nextTick` when setImmediate is unavailable in `ForEachStream`. 24 | * Resolves `ResponseBuilder` promise for the response on first call to `send` to support streaming. 25 | * Add node v0.10.x to Travis configuration. 26 | 27 | ## v0.5.4 28 | 29 | * Changes ForEachStream to use `setImmediate` instead of `process.nextTick`. 30 | 31 | ## v0.5.3 32 | 33 | * Fixes bug preventing passing of options to `bogart.batteries`. 34 | 35 | ## v0.5.2 36 | 37 | * Updates examples to use `bogart.app`. 38 | * Normalizes the return value of `bogart.router` to match other middleware. 39 | * Changes `bogart.error` middleware to match middleware conventions. 40 | * Updates `bogart.session` example to use a custom configuration key. 41 | * Allows configuration of session's `SessionIdProvider` and `SessionDataProvider` 42 | encryption key from the options parameter to `bogart.session`. 43 | * Removes `bogart.build`. 44 | * Removes `binary` middleware. 45 | * Removes `cascade` middleware. 46 | 47 | ## v0.5.1 48 | 49 | * `SessionDataProvider.loadSession` now returns the session. 50 | 51 | ## v0.5.0 52 | 53 | * Changes order of parameters to `bogart.parted` middleware to match middleware conventions. 54 | * Creates a helper module called `fsp` where node.js `fs` methods are adapted to return promises. 55 | * General housekeeping on the middleware code base to make it more maintainable. 56 | * Changes `bogart.middelware` to be a function that makes creating JSGI middleware easier. 57 | All bogart middleware is still attached to the `bogart.middleware` function as properties. 58 | * Removes the deprecated support for passing a configuration function to `bogart.router`. 59 | 60 | ## v0.3.38 61 | 62 | * Changed `bogart.response()` to `bogart.res()` to better fit design philosophy and Node conventions. 63 | 64 | ## v0.3.37 65 | 66 | * Changed signature of view engine renderer function so that the caching mechanism is readily available for view engine implementations. 67 | * ResponseBuilder helper, `bogart.response()`, can now be piped to successfully. 68 | 69 | ## v0.3.36 70 | 71 | * Publishing v0.3.35 from windows seemed to cause issues when installing on unix. Therefore; I am republishing with only a version bump from linux. 72 | 73 | ## v0.3.35 74 | 75 | * Back out change to package.json to add new CLI. 76 | 77 | ## v0.3.34 78 | 79 | * Removed dependency on Q library. 80 | 81 | ## v0.3.33 82 | 83 | * Changed the unit tests in the project from Expresso to Tap. 84 | 85 | ## v0.3.32 86 | 87 | * Did not update package.json in my tag of 0.3.31, went to 0.3.32 for consistency. 88 | 89 | ## v0.3.31 90 | 91 | * Fixed a bug where Parted middleware was not bubbling rejections. 92 | 93 | ## v0.3.30 94 | 95 | * Fixed a bug causing before callbacks to cause errors. 96 | 97 | ## v0.3.29 98 | 99 | * Missed a whenCall, same issue from v0.3.28. 100 | 101 | ## v0.3.28 102 | 103 | * Added dependency on request 2.2.9. 104 | * Removed dependency on Deflate as Node.JS 0.6.x includes zlib. 105 | * Added gzip middleware to `bogart.batteries` 106 | * Added reject callbacks for all cases where whenCall is invoked as it tries to invoke the rejectCallback even if one is not provided. 107 | 108 | ## v0.3.27 109 | 110 | * `bogart.middleware.session` assumed that `req.env` would be unique per request; however, it is not. Corrected issues caused by this. 111 | 112 | ## v0.3.26 113 | 114 | * `bogart.middleware.bodyAdapter` now adapts responses that are of type Buffer or Stream to JSGI responses. 115 | * Fixed a bug in `bogart.middleware.bodyAdatper` where Stream returns were not being handled properly. 116 | * Added `bogart.config`. The default environment is 'development' and may be overridden with the BOGART_ENV environment variable. 117 | * Expose `DefaultIdProvider` and `DefaultDataProvider` as properties of `bogart.middleware.session`. 118 | 119 | ## v0.3.25 120 | 121 | * No longer reject a return that does not include all properties of a valid JSGI response. 122 | This change facilitates Bogart as a middleware platform. 123 | * Added `bogart.middleware.batteries`, a batteries included JSGI stack for rapid application development. 124 | * The deprecated `bogart.app` has been reclaimed for the purpose of creating application stacks more easily than chaining 125 | JSGI middleware manually or using `bogart.build`. 126 | * `bogart.build` is deprecated. 127 | * Added `bogart.q` which exposes the promise implementation used by Bogart. 128 | * Added `bogart.promisify` which adapts node-style asynchronous functions to promises. 129 | * Added `bogart.proxy`, a helper to create a JSGI response that proxies a URL. 130 | * Added `viewEngine.share`, a helper for serializing JavaScript to views. See the new example in 'examples/share-javascript'. 131 | 132 | ## v0.3.24 133 | 134 | * Bug fixes in Session middleware. 135 | 136 | ## v0.3.23 137 | 138 | * Added support for string-based paths with * for splat like /foo/* 139 | * Before callbacks may now return promises that must be resolved before the route handler is executed. 140 | * After callbacks may now return promises that must be resolved before the response from the route handler is returned. 141 | 142 | ## v0.3.22 143 | 144 | * `ViewEngine` is now an `EventEmitter`. 145 | * the built-in `Mustache` view engine now emits `beforeRender` and `afterRender` events. 146 | * Fixed bugs in `Flash` middleware. 147 | 148 | ## v0.3.21 149 | 150 | * `after` had been left off of the public API of router, added it. 151 | 152 | ## v0.3.20 153 | 154 | * Simplified and corrected code for `pipe` method on the request object for the `Parted` middleware. 155 | * Added `Session` middleware. 156 | 157 | ## v0.3.19 158 | 159 | * Updated Parted dependency to 0.8.0. 160 | 161 | ## v0.3.18 162 | 163 | * Mustache partials now work properly when using layouts. 164 | 165 | ## v0.3.17 166 | 167 | * Routes now match in order added instead of longest-first. 168 | * Added `Flash` middleware to emulate the flash method of Rails. 169 | 170 | ## v0.3.16 171 | 172 | * Added `Error` middleware to translate rejected promises and thrown errors into an error response. 173 | * `Error` middleware is included by default in JSGI stacks constructed with `bogart.build`. 174 | * `ParseForm` and `ParseJson` have been replaced with `Parted` in JSGI middleware stacks constructed with `bogart.build`. 175 | 176 | ## v0.3.15 177 | 178 | * Added `Parted` middleware to take advantage of the excellent streaming parsers provided by [Parted](https://github.com/chjj/parted). 179 | * Added multipart-form example to the examples directory to demonstrate usage of the Parted middleware. 180 | 181 | ## v0.3.14 182 | 183 | * use Buffer.byteLength to determine the value for `Content-Length` headers, resolves Issue #11 184 | 185 | ## v0.3.13 186 | 187 | * `bogart.redirect` now accepts a 2nd optional parameter which if present will be merged into the returned response object 188 | 189 | ## v0.3.12 190 | 191 | * Add `before` method to the return from `bogart.router` 192 | 193 | ## v0.3.10 194 | 195 | * Added support for mustache partials to the mustache template engine. This is unrelated to Bogart partials. 196 | * Match `pathInfo` of "" to "/" if no route found for "" 197 | * Support dot in named parameters 198 | 199 | ## v0.3.9 200 | 201 | * Made view engines registerable. 202 | * Moved 'jade' and 'haml' renderers to their own packages: 'bogart-jade' and 'bogart-haml'. 203 | 204 | ## v0.3.8 205 | 206 | * Removed node-deflate dependency becuase it was sometimes failing to compile when installed with npm. 207 | -------------------------------------------------------------------------------- /Jakefile: -------------------------------------------------------------------------------- 1 | var util = require('util'); 2 | 3 | desc("generate a new bogart application skeleton"); 4 | task("generate", [], function(dir, name) { 5 | console.log("generating bogart app in " + dir); 6 | var fs = require("fs"), 7 | path = require("path"); 8 | 9 | dir = dir || "."; 10 | 11 | var appTemplate = fs.readFileSync(path.join(__dirname, "cli", "templates", "app.js.template"), 'utf8'); 12 | fs.mkdirSync(dir, 0777); 13 | fs.writeFileSync(path.join(dir, "app.js"), appTemplate, 'utf8'); 14 | 15 | var packageJsonTemplate = fs.readFileSync(path.join(__dirname, "cli", "templates", "package.json.template"), 'utf8'); 16 | fs.writeFileSync(path.join(dir, "package.json"), packageJsonTemplate, 'utf8'); 17 | }); 18 | -------------------------------------------------------------------------------- /benchmarks/alternative-routing/alt-router.js: -------------------------------------------------------------------------------- 1 | var 2 | util = require('../../lib/util'), 3 | EventEmitter = require('events').EventEmitter, 4 | Q = require('promised-io/promise'), 5 | when = Q.when, 6 | bogart = require('../../lib/bogart'); 7 | 8 | var 9 | httpMethod = { 10 | GET: "get", 11 | POST: "post", 12 | PUT: "put", 13 | DELETE: "delete" 14 | }, 15 | restMethod = { 16 | SHOW: httpMethod.GET, 17 | CREATE: httpMethod.POST, 18 | UPDATE: httpMethod.PUT, 19 | DESTROY: httpMethod.DELETE 20 | }, 21 | PATH_PARAMETER_REPLACEMENT = "([^\.\/\?]+)", 22 | PATH_PARAMETERS = /@([\w\d]+)/g; 23 | 24 | exports.bogartEvent = { 25 | BEFORE_ADD_ROUTE: "beforeAddRoute", 26 | AFTER_ADD_ROUTE: "afterAddRoute" 27 | }; 28 | 29 | exports.Router = Router; 30 | 31 | exports.init = function(config, notFoundApp) { 32 | var 33 | defaultNotFoundApp = function(req) { 34 | var body = 'Not Found'; 35 | if (req.pathInfo) { 36 | body += ': ' + req.pathInfo; 37 | } 38 | 39 | return { 40 | status: 404, 41 | body: [body], 42 | headers: { 'Content-Length': body.length, 'Content-Type': 'text/html' } 43 | }; 44 | }, 45 | router = new Router(); 46 | 47 | function bindToRouter(method) { 48 | return function() { method.apply(router, arguments); }; 49 | } 50 | config.call(router, bindToRouter(router.show), bindToRouter(router.create), bindToRouter(router.update), bindToRouter(router.destroy)); 51 | 52 | return function(req) { 53 | var resp; 54 | try { 55 | resp = router.respond(bogart.request(this, req)); 56 | if(util.no(resp) && req.pathInfo !== '/routes') { 57 | if(notFoundApp) { return notFoundApp(req); } 58 | else return defaultNotFoundApp(req); 59 | } 60 | if ((util.no(resp) || resp.status === 404)&& req.pathInfo === '/routes') { 61 | var str = 'GET
'; 62 | 63 | for(var rte in router.routes['get']) { 64 | str += '

'; 65 | str += 'path: ' + router.routes['get'][rte].path + '
' + 'paramNames: ' + router.routes['get'][rte].paramNames; 66 | str += '

'; 67 | } 68 | 69 | return { status: 200, headers: { 'Content-Length': str.length, "Content-Type": "text/html" }, body: [ str ] }; 70 | } 71 | 72 | return resp; 73 | } catch (err) { 74 | var str = 'Error
'+err.toString()+'
Stack Trace:
'+JSON.stringify(err.stack); 75 | return { status: 500, headers: { 'Content-Type': 'text/html', "Content-Length": str.length }, body: [ str ] }; 76 | } 77 | }; 78 | }; 79 | 80 | function Router() { 81 | var 82 | emitter = new EventEmitter(), 83 | settings = {}, k; 84 | 85 | for (k in emitter) { 86 | this[k] = emitter[k]; 87 | } 88 | 89 | this.setting = function(name, val) { 90 | if (val === undefined) { 91 | return settings[name]; 92 | } 93 | 94 | settings[name] = val; 95 | return this; 96 | }; 97 | this.routes = {}; 98 | } 99 | 100 | /** 101 | * Register a route 102 | * @param {String} method Http Verb e.g. 'GET', 'POST', 'PUT', 'DELETE' 103 | * @param {String} path Path for the route 104 | * @param {Function} handler Function to execute when the route is accessed 105 | */ 106 | Router.prototype.route = function(method, path, handler) { 107 | var paramNames, route, originalPath = path; 108 | 109 | if (path.constructor === String) { 110 | paramNames = path.match(PATH_PARAMETERS) || []; 111 | paramNames = paramNames.map(function(x) { return x.substring(1); }); 112 | 113 | path = new RegExp(path.replace(/\./, '\\.').replace(PATH_PARAMETERS, PATH_PARAMETER_REPLACEMENT)); 114 | } 115 | 116 | route = { path: path, paramNames: paramNames, handler: handler, originalPath: originalPath, pathName: originalPath.replace(/@[^\/\?]+/g, "@PARAM")}; 117 | 118 | this.emit(exports.bogartEvent.BEFORE_ADD_ROUTE, this, route); 119 | 120 | this.routes[method] = this.routes[method] || {}; 121 | this.routes[method][route.pathName] = route; 122 | 123 | this.emit(exports.bogartEvent.AFTER_ADD_ROUTE, this, route); 124 | 125 | return this; 126 | }; 127 | 128 | Router.prototype.handler = function(verb, path) { 129 | return this.routes[verb.toLowerCase()] ? this.routes[verb.toLowerCase()][path.replace(/@[^\/\?]+/g, "@PARAM")] : null; 130 | }; 131 | 132 | Router.prototype.respond = function(reqPromise) { 133 | var 134 | self = this; 135 | 136 | return when(reqPromise, function(req) { 137 | var 138 | route = self.handler(req.method, req.pathInfo), 139 | routeParams = Object.create(Object.prototype), 140 | routeParamValues = null; 141 | 142 | if (util.no(route)) { 143 | return null; 144 | } 145 | 146 | routeParamValues = route.path.exec(req.pathInfo); 147 | if (routeParamValues) { 148 | routeParamValues.shift(); // Remove the initial match 149 | 150 | routeParamValues.forEach(function(val, indx) { 151 | val = decodeURIComponent(val); 152 | val = val.substring(1); 153 | if (route.paramNames.length > indx) { 154 | routeParams[route.paramNames[indx]] = val; 155 | } else if (val !== undefined) { 156 | routeParams.splat = routeParams.splat || []; 157 | routeParams.splat.push(val); 158 | } 159 | }); 160 | 161 | // console.log(routeParams); 162 | } 163 | 164 | Object.defineProperty(req, 'routeParams', { value: routeParams, enumerable: true, readonly: true }); 165 | Object.defineProperty(req, 'params', { value: util.merge({}, req.routeParams, req.search, req.body), enumerable: true, readonly: true }); 166 | 167 | var hold = []; 168 | for(var name in routeParams) { 169 | hold.push(routeParams[name]); 170 | } 171 | 172 | var handlerResp = route.handler.apply(self, [req].concat(hold || [])); 173 | if (util.no(handlerResp)) { 174 | throw new Error("Handler returned empty response:" + JSON.stringify(route)); 175 | } 176 | 177 | return handlerResp; 178 | }); 179 | }; 180 | 181 | Router.prototype.show = function(path, handler) { 182 | return this.route(restMethod.SHOW, path, handler); 183 | }; 184 | 185 | Router.prototype.create = function(path, handler) { 186 | return this.route(restMethod.CREATE, path, handler); 187 | }; 188 | 189 | Router.prototype.update = function(path, handler) { 190 | return this.route(restMethod.UPDATE, path, handler); 191 | }; 192 | 193 | Router.prototype.destroy = function(path, handler) { 194 | return this.route(restMethod.DESTROY, path, handler); 195 | }; -------------------------------------------------------------------------------- /benchmarks/alternative-routing/app.js: -------------------------------------------------------------------------------- 1 | var 2 | bogart = require('../../lib/bogart'), 3 | router = require('./alt-router'), 4 | sys = require('sys'); 5 | 6 | var config = function(show, create, update, destroy) { 7 | show('/hello/@firstname/@lastname', function(req, fname, lname) { 8 | return bogart.html('Hello '+fname + " "+lname); 9 | }); 10 | 11 | // show('/stream', function(req) { 12 | // var streamer = bogart.stream(); 13 | // 14 | // setInterval(function() { 15 | // var currentTime = new Date(); 16 | // streamer(currentTime.getHours()+':'+currentTime.getMinutes()+':'+currentTime.getSeconds()+"\n"); 17 | // }, 10); 18 | // 19 | // setTimeout(function() { 20 | // streamer.end(); 21 | // }, 10000); 22 | // 23 | // return streamer.respond(); 24 | // }); 25 | 26 | show('/1179/@fname/@lname', function(req, fname, lname) { return bogart.html('Hello '+fname+' '+lname); }); 27 | show('/9398/@fname/@lname', function(req, fname, lname) { return bogart.html('Hello '+fname+' '+lname); }); 28 | show('/680/@fname/@lname', function(req, fname, lname) { return bogart.html('Hello '+fname+' '+lname); }); 29 | show('/8617/@fname/@lname', function(req, fname, lname) { return bogart.html('Hello '+fname+' '+lname); }); 30 | show('/4008/@fname/@lname', function(req, fname, lname) { return bogart.html('Hello '+fname+' '+lname); }); 31 | show('/1425/@fname/@lname', function(req, fname, lname) { return bogart.html('Hello '+fname+' '+lname); }); 32 | show('/9404/@fname/@lname', function(req, fname, lname) { return bogart.html('Hello '+fname+' '+lname); }); 33 | show('/3896/@fname/@lname', function(req, fname, lname) { return bogart.html('Hello '+fname+' '+lname); }); 34 | show('/7512/@fname/@lname', function(req, fname, lname) { return bogart.html('Hello '+fname+' '+lname); }); 35 | show('/3543/@fname/@lname', function(req, fname, lname) { return bogart.html('Hello '+fname+' '+lname); }); 36 | show('/5685/@fname/@lname', function(req, fname, lname) { return bogart.html('Hello '+fname+' '+lname); }); 37 | show('/2109/@fname/@lname', function(req, fname, lname) { return bogart.html('Hello '+fname+' '+lname); }); 38 | show('/332/@fname/@lname', function(req, fname, lname) { return bogart.html('Hello '+fname+' '+lname); }); 39 | show('/8617/@fname/@lname', function(req, fname, lname) { return bogart.html('Hello '+fname+' '+lname); }); 40 | show('/6889/@fname/@lname', function(req, fname, lname) { return bogart.html('Hello '+fname+' '+lname); }); 41 | show('/8274/@fname/@lname', function(req, fname, lname) { return bogart.html('Hello '+fname+' '+lname); }); 42 | show('/8289/@fname/@lname', function(req, fname, lname) { return bogart.html('Hello '+fname+' '+lname); }); 43 | show('/4454/@fname/@lname', function(req, fname, lname) { return bogart.html('Hello '+fname+' '+lname); }); 44 | show('/4305/@fname/@lname', function(req, fname, lname) { return bogart.html('Hello '+fname+' '+lname); }); 45 | show('/5223/@fname/@lname', function(req, fname, lname) { return bogart.html('Hello '+fname+' '+lname); }); 46 | show('/4602/@fname/@lname', function(req, fname, lname) { return bogart.html('Hello '+fname+' '+lname); }); 47 | show('/3084/@fname/@lname', function(req, fname, lname) { return bogart.html('Hello '+fname+' '+lname); }); 48 | show('/1491/@fname/@lname', function(req, fname, lname) { return bogart.html('Hello '+fname+' '+lname); }); 49 | show('/2727/@fname/@lname', function(req, fname, lname) { return bogart.html('Hello '+fname+' '+lname); }); 50 | show('/4104/@fname/@lname', function(req, fname, lname) { return bogart.html('Hello '+fname+' '+lname); }); 51 | show('/664/@fname/@lname', function(req, fname, lname) { return bogart.html('Hello '+fname+' '+lname); }); 52 | show('/2042/@fname/@lname', function(req, fname, lname) { return bogart.html('Hello '+fname+' '+lname); }); 53 | show('/4168/@fname/@lname', function(req, fname, lname) { return bogart.html('Hello '+fname+' '+lname); }); 54 | show('/2583/@fname/@lname', function(req, fname, lname) { return bogart.html('Hello '+fname+' '+lname); }); 55 | show('/490/@fname/@lname', function(req, fname, lname) { return bogart.html('Hello '+fname+' '+lname); }); 56 | show('/6060/@fname/@lname', function(req, fname, lname) { return bogart.html('Hello '+fname+' '+lname); }); 57 | show('/9095/@fname/@lname', function(req, fname, lname) { return bogart.html('Hello '+fname+' '+lname); }); 58 | show('/3441/@fname/@lname', function(req, fname, lname) { return bogart.html('Hello '+fname+' '+lname); }); 59 | show('/8376/@fname/@lname', function(req, fname, lname) { return bogart.html('Hello '+fname+' '+lname); }); 60 | show('/8906/@fname/@lname', function(req, fname, lname) { return bogart.html('Hello '+fname+' '+lname); }); 61 | show('/4239/@fname/@lname', function(req, fname, lname) { return bogart.html('Hello '+fname+' '+lname); }); 62 | show('/2094/@fname/@lname', function(req, fname, lname) { return bogart.html('Hello '+fname+' '+lname); }); 63 | show('/2338/@fname/@lname', function(req, fname, lname) { return bogart.html('Hello '+fname+' '+lname); }); 64 | show('/8557/@fname/@lname', function(req, fname, lname) { return bogart.html('Hello '+fname+' '+lname); }); 65 | show('/6740/@fname/@lname', function(req, fname, lname) { return bogart.html('Hello '+fname+' '+lname); }); 66 | show('/1321/@fname/@lname', function(req, fname, lname) { return bogart.html('Hello '+fname+' '+lname); }); 67 | show('/3768/@fname/@lname', function(req, fname, lname) { return bogart.html('Hello '+fname+' '+lname); }); 68 | show('/7059/@fname/@lname', function(req, fname, lname) { return bogart.html('Hello '+fname+' '+lname); }); 69 | show('/1431/@fname/@lname', function(req, fname, lname) { return bogart.html('Hello '+fname+' '+lname); }); 70 | show('/9329/@fname/@lname', function(req, fname, lname) { return bogart.html('Hello '+fname+' '+lname); }); 71 | show('/5440/@fname/@lname', function(req, fname, lname) { return bogart.html('Hello '+fname+' '+lname); }); 72 | show('/4610/@fname/@lname', function(req, fname, lname) { return bogart.html('Hello '+fname+' '+lname); }); 73 | show('/2181/@fname/@lname', function(req, fname, lname) { return bogart.html('Hello '+fname+' '+lname); }); 74 | show('/6647/@fname/@lname', function(req, fname, lname) { return bogart.html('Hello '+fname+' '+lname); }); 75 | show('/2342/@fname/@lname', function(req, fname, lname) { return bogart.html('Hello '+fname+' '+lname); }); 76 | show('/7792/@fname/@lname', function(req, fname, lname) { return bogart.html('Hello '+fname+' '+lname); }); 77 | show('/7018/@fname/@lname', function(req, fname, lname) { return bogart.html('Hello '+fname+' '+lname); }); 78 | show('/113/@fname/@lname', function(req, fname, lname) { return bogart.html('Hello '+fname+' '+lname); }); 79 | show('/3956/@fname/@lname', function(req, fname, lname) { return bogart.html('Hello '+fname+' '+lname); }); 80 | show('/6116/@fname/@lname', function(req, fname, lname) { return bogart.html('Hello '+fname+' '+lname); }); 81 | show('/1305/@fname/@lname', function(req, fname, lname) { return bogart.html('Hello '+fname+' '+lname); }); 82 | show('/3673/@fname/@lname', function(req, fname, lname) { return bogart.html('Hello '+fname+' '+lname); }); 83 | show('/1411/@fname/@lname', function(req, fname, lname) { return bogart.html('Hello '+fname+' '+lname); }); 84 | show('/7164/@fname/@lname', function(req, fname, lname) { return bogart.html('Hello '+fname+' '+lname); }); 85 | show('/6766/@fname/@lname', function(req, fname, lname) { return bogart.html('Hello '+fname+' '+lname); }); 86 | show('/9508/@fname/@lname', function(req, fname, lname) { return bogart.html('Hello '+fname+' '+lname); }); 87 | show('/9726/@fname/@lname', function(req, fname, lname) { return bogart.html('Hello '+fname+' '+lname); }); 88 | show('/7935/@fname/@lname', function(req, fname, lname) { return bogart.html('Hello '+fname+' '+lname); }); 89 | show('/9898/@fname/@lname', function(req, fname, lname) { return bogart.html('Hello '+fname+' '+lname); }); 90 | show('/4478/@fname/@lname', function(req, fname, lname) { return bogart.html('Hello '+fname+' '+lname); }); 91 | show('/5212/@fname/@lname', function(req, fname, lname) { return bogart.html('Hello '+fname+' '+lname); }); 92 | show('/1110/@fname/@lname', function(req, fname, lname) { return bogart.html('Hello '+fname+' '+lname); }); 93 | show('/3937/@fname/@lname', function(req, fname, lname) { return bogart.html('Hello '+fname+' '+lname); }); 94 | show('/2833/@fname/@lname', function(req, fname, lname) { return bogart.html('Hello '+fname+' '+lname); }); 95 | show('/4712/@fname/@lname', function(req, fname, lname) { return bogart.html('Hello '+fname+' '+lname); }); 96 | show('/4554/@fname/@lname', function(req, fname, lname) { return bogart.html('Hello '+fname+' '+lname); }); 97 | show('/233/@fname/@lname', function(req, fname, lname) { return bogart.html('Hello '+fname+' '+lname); }); 98 | show('/255/@fname/@lname', function(req, fname, lname) { return bogart.html('Hello '+fname+' '+lname); }); 99 | show('/3202/@fname/@lname', function(req, fname, lname) { return bogart.html('Hello '+fname+' '+lname); }); 100 | show('/8267/@fname/@lname', function(req, fname, lname) { return bogart.html('Hello '+fname+' '+lname); }); 101 | show('/6722/@fname/@lname', function(req, fname, lname) { return bogart.html('Hello '+fname+' '+lname); }); 102 | show('/1157/@fname/@lname', function(req, fname, lname) { return bogart.html('Hello '+fname+' '+lname); }); 103 | show('/2224/@fname/@lname', function(req, fname, lname) { return bogart.html('Hello '+fname+' '+lname); }); 104 | show('/4185/@fname/@lname', function(req, fname, lname) { return bogart.html('Hello '+fname+' '+lname); }); 105 | show('/5762/@fname/@lname', function(req, fname, lname) { return bogart.html('Hello '+fname+' '+lname); }); 106 | show('/1187/@fname/@lname', function(req, fname, lname) { return bogart.html('Hello '+fname+' '+lname); }); 107 | show('/2963/@fname/@lname', function(req, fname, lname) { return bogart.html('Hello '+fname+' '+lname); }); 108 | show('/1876/@fname/@lname', function(req, fname, lname) { return bogart.html('Hello '+fname+' '+lname); }); 109 | show('/8034/@fname/@lname', function(req, fname, lname) { return bogart.html('Hello '+fname+' '+lname); }); 110 | show('/2134/@fname/@lname', function(req, fname, lname) { return bogart.html('Hello '+fname+' '+lname); }); 111 | show('/5503/@fname/@lname', function(req, fname, lname) { return bogart.html('Hello '+fname+' '+lname); }); 112 | show('/9011/@fname/@lname', function(req, fname, lname) { return bogart.html('Hello '+fname+' '+lname); }); 113 | show('/565/@fname/@lname', function(req, fname, lname) { return bogart.html('Hello '+fname+' '+lname); }); 114 | show('/5069/@fname/@lname', function(req, fname, lname) { return bogart.html('Hello '+fname+' '+lname); }); 115 | show('/5060/@fname/@lname', function(req, fname, lname) { return bogart.html('Hello '+fname+' '+lname); }); 116 | show('/2986/@fname/@lname', function(req, fname, lname) { return bogart.html('Hello '+fname+' '+lname); }); 117 | show('/2009/@fname/@lname', function(req, fname, lname) { return bogart.html('Hello '+fname+' '+lname); }); 118 | show('/1376/@fname/@lname', function(req, fname, lname) { return bogart.html('Hello '+fname+' '+lname); }); 119 | show('/8790/@fname/@lname', function(req, fname, lname) { return bogart.html('Hello '+fname+' '+lname); }); 120 | show('/8756/@fname/@lname', function(req, fname, lname) { return bogart.html('Hello '+fname+' '+lname); }); 121 | show('/9349/@fname/@lname', function(req, fname, lname) { return bogart.html('Hello '+fname+' '+lname); }); 122 | show('/1622/@fname/@lname', function(req, fname, lname) { return bogart.html('Hello '+fname+' '+lname); }); 123 | show('/1899/@fname/@lname', function(req, fname, lname) { return bogart.html('Hello '+fname+' '+lname); }); 124 | show('/9071/@fname/@lname', function(req, fname, lname) { return bogart.html('Hello '+fname+' '+lname); }); 125 | show('/6736/@fname/@lname', function(req, fname, lname) { return bogart.html('Hello '+fname+' '+lname); }); 126 | 127 | 128 | 129 | }; 130 | 131 | bogart.start(router.init(config)); -------------------------------------------------------------------------------- /benchmarks/hello-world/hello-world.js: -------------------------------------------------------------------------------- 1 | var bogart = require('../../lib/bogart'); 2 | 3 | var router = bogart.router(); 4 | router.get('/', function(req) { 5 | return bogart.html('Hello World'); 6 | }); 7 | 8 | bogart.start(router); 9 | -------------------------------------------------------------------------------- /benchmarks/mustache-layout/app.js: -------------------------------------------------------------------------------- 1 | var 2 | bogart = require('../../lib/bogart'), 3 | path = require('path') 4 | 5 | var app = bogart.router(function(show) { 6 | var viewEngine = bogart.viewEngine('mustache', path.join(bogart.maindir(), '..', 'views')); 7 | 8 | show('/', function(req, res) { 9 | return viewEngine.respond('index.html', { locals: { description: 'This is content' } }); 10 | }); 11 | }); 12 | 13 | bogart.start(app); 14 | -------------------------------------------------------------------------------- /benchmarks/route-generator.js: -------------------------------------------------------------------------------- 1 | /* 2 | USAGE: node route-generator.js [vaiable prefix] [number of routes to generate] >> generated-routes.txt 3 | */ 4 | 5 | 6 | var head = "show('/", 7 | tail = "/"+process.argv[2]+"fname/"+process.argv[2]+"lname', function(req, fname, lname) { return bogart.html('Hello '+fname+' '+lname); });"; 8 | 9 | for(var i = 0; i 9 | # 10 | # 11 | # 12 | 13 | log(){ 14 | echo "... $@" 15 | } 16 | 17 | # 18 | # Benchmark the given and [path]. 19 | # 20 | # [path] 21 | # 22 | 23 | bm(){ 24 | local dir=$1; 25 | local path=${2-/}; 26 | for file in benchmarks/$dir/*; do 27 | log running $file 28 | case $file in 29 | *.js) 30 | node $file & 31 | sleep 2 32 | ;; 33 | *.thin.ru) 34 | thin -R $file -p 3000 start & 35 | sleep 2 36 | ;; 37 | *.mongrel.ru) 38 | rackup $file -p 3000 -s mongrel & 39 | sleep 2 40 | ;; 41 | esac 42 | local pid=$! 43 | local dirname=results/$(dirname $file) 44 | mkdir -p $dirname 45 | $AB $ABFLAGS -g results/$file.dat $ADDR/$path > results/$file.out 46 | log $(cat results/$file.out | grep Requests) 47 | kill -9 $pid 48 | done 49 | } 50 | 51 | # Make ./results 52 | mkdir -p results 53 | 54 | # Store flags 55 | echo $ABFLAGS > results/flags 56 | 57 | # Run benchmarks 58 | log $AB $ABFLAGS $ADDR 59 | bm hello-world 60 | #bm alternative-routing 61 | bm mustache-layout 62 | -------------------------------------------------------------------------------- /benchmarks/views/index.html: -------------------------------------------------------------------------------- 1 |

2 | {{ description }} 3 |

4 | -------------------------------------------------------------------------------- /benchmarks/views/layout.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Mustache with Layout 4 | 5 | 6 |

Hello

7 | {{{ body }}} 8 | 9 | 10 | -------------------------------------------------------------------------------- /cli/templates/app.js.template: -------------------------------------------------------------------------------- 1 | /** 2 | * A bogart application skeleton 3 | */ 4 | 5 | var bogart = require("bogart"); 6 | 7 | var homeRouter = bogart.router(function(get, post, put, del) { 8 | get("/", function() { 9 | return bogart.html("Hello World"); 10 | }); 11 | }); 12 | 13 | bogart.start(homeRouter); 14 | -------------------------------------------------------------------------------- /cli/templates/package.json.template: -------------------------------------------------------------------------------- 1 | { 2 | "author": "", 3 | "email": "", 4 | "version": "0.0.1", 5 | "directories": { 6 | "lib": "./lib" 7 | }, 8 | "main": "./app.js", 9 | dependencies: { 10 | "bogart": ">=0.1.1" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /docs/app.md: -------------------------------------------------------------------------------- 1 | # App 2 | 3 | The Bogart Application constructor, `bogart.app`, exposes a helper for creating JSGI 4 | servers. 5 | 6 | ## app.start([port], [hostname]) 7 | 8 | Start a server to accept connections on port and hostname. If hostname is ommitted, 9 | connections on any IPv4 address will be accepted. 10 | 11 | This function is asynchronous and returns a promise. 12 | 13 | ## app.use(middleware) 14 | 15 | Add a JSGI application (middleware) or Bogart Router to the server's JSGI stack. 16 | 17 | ## Event: 'beforeAddMiddleware' 18 | `function(app, args) { }` 19 | 20 | Emitted before `app.use` inserts the application or router into the JSGI stack. 21 | 22 | ## Event: 'afterAddMiddleware' 23 | `function(app, middleware) { }` 24 | 25 | Emitted after `app.use` inserts the application or router into the JSGI stack. 26 | -------------------------------------------------------------------------------- /docs/bogart.md: -------------------------------------------------------------------------------- 1 | # Bogart 2 | 3 | ## Utility 4 | 5 | ### bogart.q 6 | 7 | Namespace for promise functions. See the q module documentation for more information. 8 | 9 | ### bogart.promisify(nodeAsynchronousFunction, context) 10 | 11 | Adapts a NodeJS style asynchronous function to return a promise. 12 | A NodeJS style asynchronous function is a function that takes as its last parameter a callback. 13 | The callback expects that the first parameter when it is called will be an error if it exists 14 | or falsey if there is no error. The second parameter is the success value for the callback. 15 | 16 | Promisify works on NodeJS asynchronous functions that only call their callback one time. If a 17 | functions calls its callback multiple times, it is not a candidate to be wrapped in a promise as 18 | promises may only be resolved one time. 19 | 20 | ## Core 21 | 22 | ### bogart.app() 23 | 24 | Create a Bogart Application, a helper for composing stacks of JSGI middleware and Bogart routers. 25 | 26 | Please see [Application](/app/) for more information. 27 | 28 | ### bogart.router([config], nextApp) 29 | 30 | Creates a Bogart Router, a JSGI middleware appliance that makes it easy to execute functions 31 | depending on the request path. 32 | 33 | Please see [Routing](/routing/) for more information. 34 | 35 | ### bogart.start(jsgiApp, [options]) 36 | 37 | Begin listening on the default port, 8080, or the port specified in options.port if options are provided. 38 | jsgiApp should be a JSGI function that serves as the entry point for the server. 39 | 40 | ### bogart.viewEngine(engine) 41 | 42 | Forwarded from `view.viewEngine`. Please see the [View Engine documentation](/view-engine/) for more information. 43 | 44 | ## View Helpers 45 | 46 | ### bogart.error(msg, opts) 47 | 48 | Creates a JSGI response with a body of msg, a "Content-Type" of "text/html", and 49 | a status of 500. The options parameter will be merged with the resulting JSGI object if 50 | provided to allow overriding or addition of other properties. 51 | 52 | ### bogart.html(html, [options]) 53 | 54 | Creates a JSGI response with a body of html. The options parameter may be used to 55 | override properties of the JSGI response like headers and status. 56 | 57 | ### bogart.text(str, [options]) 58 | 59 | Create a JSGI response with a "Content-Type" of "text/plain" and a body containing str. The 60 | options parameter will be merged with the resulting JSGI object if provided to allow overriding 61 | or addition of other properties. 62 | 63 | ### bogart.json(obj, [options]) 64 | 65 | Creates a JSGI response with a "Content-Type" of "application/json" and a body containing the 66 | JSON representation of obj. The options parameter will be merged with the resulting JSGI object 67 | if provided to allow overriding or addition of other properties. 68 | 69 | ### bogart.redirect(url, [options]) 70 | 71 | Creates a JSGI response with a status of 302, temporary redirect. The options parameter will be 72 | merged with the resulting JSGI object if provided to allow overriding or addition of other 73 | properties. 74 | 75 | ### bogart.response([viewEngine]) 76 | 77 | Instantiates a ResponseBuilder, a utility object that aids in constructing responses imperatively. 78 | 79 | ### bogart.permanentRedirect(url, [options]) 80 | 81 | Creates a JSGI response with a status of 301, permanent redirect. The options parameter will be 82 | merged with the resulting JSGI object if provided to allow overriding or addition of other 83 | properties. 84 | 85 | ### bogart.notModified([options]) 86 | 87 | Creates a JSGI response with a status of 304, not modified. The options parameter will be 88 | merged with the resulting JSGI object if provided to allow overriding or addition of other 89 | properties. 90 | 91 | ## ResponseBuilder 92 | 93 | ResponseBuilder is returned by `bogart.response`. ResponseBuilder is a utilty class 94 | for composing JSGI responses imperatively. 95 | 96 | ### ResponseBuilder.end() 97 | 98 | The end method must be called when you are finished constructing the response. 99 | 100 | ### ResponseBuilder.headers(headers) 101 | 102 | Overwrite the response headers. 103 | 104 | ### ResponseBuilder.render(view, [options]) 105 | 106 | Renders view to the response body. If a View Engine was not provided, raises an error. 107 | 108 | ### ResponseBuilder.status(num) 109 | 110 | Set the status of the response to num. 111 | 112 | ### ResponseBuilder.send(content) 113 | 114 | Add content to the response body. 115 | 116 | ### ResponseBuilder.setHeader(header, value) 117 | 118 | Set header to value. 119 | 120 | ### ResponseBuilder.statusCode 121 | 122 | Property for getting and setting the status code of the response. -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | # Bogart 2 | 3 | ## Features 4 | 5 | * Intuitive Routing 6 | * Extensible View Engine that supports Jade, Mustache, and HAML out of the box 7 | * View Engine supports partials 8 | * [JSGI](http://wiki.commonjs.org/wiki/JSGI) compatible 9 | * Good test coverage 10 | * Fast! 11 | 12 | ## Learn 13 | 14 | * [Routing]() Setup Bogart routes to handle URLs. 15 | * [ViewEngine]() Extensible ViewEngine with partials and layout support. 16 | * [Middleware]() Compose responses with reusable middleware. -------------------------------------------------------------------------------- /docs/routing.md: -------------------------------------------------------------------------------- 1 | # Routing 2 | 3 | Routing in Bogart is simple and intuitive. A route is a HTTP method paried with a 4 | URL matching pattern and a function to call to handle requests to the route. 5 | 6 | var router = bogart.router(); 7 | router.get('/', function(req) { 8 | return bogart.html('Hello World'); 9 | }); 10 | 11 | Routes are tested for matches in the order in which they were defined. 12 | 13 | ## Route Patterns 14 | 15 | Route patterns are matched vs URLs. They may include named parameters that will 16 | be accessible via the `params` object of the `req` object passed to the route handler. 17 | 18 | var router = bogart.router(); 19 | router.get('/hello/:name', function(req) { 20 | var greeting = 'Hello '+req.params.name; 21 | return bogart.html(greeting); 22 | }); 23 | 24 | It is also possible to access named parameters via arguments passed to the handler function. 25 | Named parameters will be passed in the order they are speicifed in the route pattern. 26 | 27 | var router = bogart.router(); 28 | router.get('/hello/:name', function(req, name) { 29 | return bogart.html('Hello '+name); 30 | }); 31 | 32 | Route patterns support wildcards. Wildcards will match anything whereas regular named parameters 33 | will not match beyond a path separator ("/"). 34 | 35 | var router = bogart.router(); 36 | router.get('/hello/*', function(req, name) { 37 | return bogart.html('Hello '+req.params.splat[0]); 38 | }); 39 | 40 | ## Regex Routes 41 | 42 | When a route pattern is not powerful enough, regular expressions may be used to specify which 43 | URLs are to be matched. 44 | 45 | var app = bogart.router(); 46 | app.get(/\/posts?/, function(req) { 47 | // Matches 'post' or 'posts' 48 | return bogart.html('Regex Route'); 49 | }); 50 | 51 | Parameters are via regular expression groups in regular expression routes. The parameter values 52 | are put in an `Array` in `req.params.splat` of the `req` object passed to the route handler. 53 | 54 | var app = bogart.router(); 55 | app.get(/hello-(.*)/, function(req) { 56 | var name = req.params.splat[0]; 57 | return bogart.html('Hello '+name); 58 | }); 59 | -------------------------------------------------------------------------------- /docs/view-engine.md: -------------------------------------------------------------------------------- 1 | # View Engine 2 | 3 | The Bogart `ViewEngine` renders view. It handles partials and layouts as well. 4 | The `ViewEngine` ships with support for [Mustache](http://mustache.github.com/), 5 | [Jade](http://jade-lang.com/), and [Haml](http://haml-lang.com/). 6 | 7 | ## view.viewEngine(engine) 8 | 9 | Creates a `view.ViewEngine`. 10 | 11 | ## ViewEngine.render(template, [options]) 12 | 13 | The `render` method uses the selected templating engine to render the specified view 14 | with options specified in the options `locals` object. 15 | 16 | Rendering a view with mustache and replacement variables: 17 | 18 | var viewEngine = bogart.viewEngine('mustache'); 19 | viewEngine.render('index.html', { locals: { title: 'Hello Mustache' } }); 20 | 21 | ## ViewEngine.respond(template, [options]) 22 | 23 | The `respond` method returns a promise for a JSGI response that will render the 24 | specified view. 25 | 26 | var viewEngine = bogart.viewEngine('mustache'); 27 | viewEngine.respond('index.html', { locals: { title: 'Hello Mustache' } }); 28 | 29 | ## view.RenderOptions 30 | 31 | Options passed to `viewEngine.render` and `viewEngine.respond` should quack like view.RenderOptions. 32 | 33 | * opts.layout The name of the layout or a boolean. If `true` then the name is defaulted to 'layout.html'. If `false` then no layout should be used. 34 | * opts.locals Context in which to render the template. This should contain the replacement values of variables in your templates. 35 | 36 | ## Layouts 37 | 38 | Layouts are supported. By default, if there is a file named 'layout.{ext}' where 39 | `ext` is the extension of the view (haml, jade, mustache, html, etc...) then this file 40 | is used as the layout for the view. The layout is expected to render a variable named 41 | `body`. The `body` variable will contain the contents of view being rendered into the 42 | layout. The `body` variable should not be HTML escaped. 43 | 44 | Specifying the file extension of a template is optional as long as the file extension 45 | matches the default file extension associated with the template type. For example, if 46 | using Jade then `viewEngine.render('index')` is equivalent to `viewEngine.render('index.jade')`. 47 | 48 | ### Mustache Example 49 | 50 | Mustache is the default template engine of Bogart. 51 | 52 | View (index.html): 53 | 54 | Welcome {{firstName}}! 55 | 56 | Layout (layout.html): 57 | 58 | 59 | {{title}} 60 | 61 | 62 |

{{title}}

63 | 64 | {{{body}}} 65 | 66 | 67 | 68 | App (app.js): 69 | 70 | var bogart = require('bogart'); 71 | var viewEngine = bogart.viewEngine('mustache'); 72 | var router = bogart.router(); 73 | 74 | router.get('/hello/:firstName', function(req) { 75 | return viewEngine.respond('index.html', { 76 | locals: { title: 'Hello', firstName: req.params.firstName } 77 | }); 78 | }); 79 | 80 | bogart.start(router); 81 | 82 | Execute `node app.js` and visit [http://localhost:8080/hello/bogart](http://localhost:8080/hello/bogart). 83 | 84 | ### Jade Example 85 | 86 | Make sure to install bogart-jade from npm before running these examples. 87 | 88 | View (index.jade): 89 | 90 | Welcome #{firstName}! 91 | 92 | Layout (layout.jade): 93 | 94 | html 95 | head 96 | title #{title} 97 | body 98 | h1 #{title} 99 | !{body} 100 | 101 | App (app.js): 102 | 103 | var bogart = require('bogart'); 104 | var viewEngine = bogart.viewEngine('jade'); 105 | var router = bogart.router(); 106 | 107 | router.get('/hello/:firstName', function(req) { 108 | return viewEngine.respond('index', { 109 | locals: { title: 'Hello', firstName: req.params.firstName } 110 | }); 111 | }); 112 | 113 | bogart.start(router); 114 | 115 | Execute `node app.js` and visit [http://localhost:8080/hello/bogart](http://localhost:8080/hello/bogart). 116 | 117 | ## Partials 118 | 119 | Sometimes, especially with AJAX requests, it is desirable to return a view without rendering it inside of a 120 | layout even if a layout is present. To acccomodate this, the View Engine has a `partial` method. The `partial` 121 | method takes the same arguments as the `render` method but does not render its template insdie of 122 | a layout. 123 | 124 | var viewEngine = bogart.viewEngine('mustache'); 125 | viewEngine.partial('index.html'); 126 | 127 | -------------------------------------------------------------------------------- /engines/node/lib/fileWatcher.js: -------------------------------------------------------------------------------- 1 | exports.watchFile = require('fs').watchFile; -------------------------------------------------------------------------------- /examples/body-adapter.js: -------------------------------------------------------------------------------- 1 | var bogart = require('../lib/bogart') 2 | , util = require('util') 3 | , Stream = require('stream').Stream; 4 | 5 | function HelloWorldStream() { 6 | var self = this; 7 | 8 | Stream.call(this); 9 | 10 | this.readable = true; 11 | 12 | process.nextTick(function() { 13 | self.emit('data', 'Stream'); 14 | process.nextTick(function() { 15 | self.emit('data', 16 | 'This is an example of a Bogart route that returns a stream. '+ 17 | 'See a route that returns a Buffer'); 18 | self.emit('end'); 19 | }); 20 | }); 21 | } 22 | 23 | util.inherits(HelloWorldStream, Stream); 24 | 25 | var router = bogart.router(); 26 | router.get('/', function() { 27 | return new HelloWorldStream(); 28 | }); 29 | 30 | router.get('/buffer', function() { 31 | return new Buffer('Buffer' + 32 | 'This is an example of a Bogart route that returns a buffer.'); 33 | }); 34 | 35 | var app = bogart.app(); 36 | app.use(bogart.middleware.bodyAdapter); 37 | app.use(router); 38 | 39 | app.start(); -------------------------------------------------------------------------------- /examples/config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Bogart `config` example. 3 | * 4 | * Environment is determined by the OS environment variable BOGART_ENV. 5 | * The default value for environment if BOGART_ENV is not set is 'development'. 6 | */ 7 | 8 | var bogart = require('../lib/bogart'); 9 | 10 | function developmentApp() { 11 | return function(req) { 12 | return 'You are running in development mode.'; 13 | }; 14 | }; 15 | 16 | function productionApp() { 17 | return function(req) { 18 | return 'You are running in production mode.'; 19 | }; 20 | }; 21 | 22 | var app = bogart.app(); 23 | 24 | bogart.config(function() { 25 | // Executed in all environments. 26 | app.use(bogart.batteries); 27 | }); 28 | 29 | bogart.config('development', function() { 30 | app.use(developmentApp); 31 | }); 32 | 33 | bogart.config('production', function() { 34 | app.use(productionApp); 35 | }); 36 | 37 | app.start(); -------------------------------------------------------------------------------- /examples/cors.js: -------------------------------------------------------------------------------- 1 | var bogart = require('../lib/bogart'); 2 | 3 | var router = bogart.router(); 4 | router.get('/', function(req) { 5 | return 'Hello Root'; 6 | }); 7 | 8 | router.get('/:name', function(req) { 9 | return bogart.cors({message:'Hello, ' + req.params.name + '!'}); 10 | }); 11 | 12 | var app = bogart.app(); 13 | app.use(bogart.batteries); 14 | app.use(router); 15 | 16 | app.start(); 17 | 18 | /* From another domain: 19 | * 20 | * $.ajax({ 21 | * url: 'http://localhost:8080/Duncan', 22 | * success: function(data) { 23 | * console.log(data); 24 | * } 25 | * }); 26 | * 27 | */ -------------------------------------------------------------------------------- /examples/facebook-authentication/app.js: -------------------------------------------------------------------------------- 1 | const PORT = 1337; 2 | 3 | var path = require('path'); 4 | var bogart = require('../../lib/bogart'); 5 | 6 | var config = require('./config.json'); 7 | 8 | var facebookConfig = { 9 | clientId: config.appId, 10 | clientSecret: config.secret, 11 | host: 'http://localhost:'+PORT 12 | }; 13 | 14 | var router = bogart.router(); 15 | router.get('/profile', function(req) { 16 | return bogart.html(JSON.stringify(req.session('profile'))); 17 | }); 18 | 19 | var frontRouter = bogart.router(); 20 | frontRouter.get('/', function(req) { 21 | return bogart.html('Profile | Login'); 22 | }); 23 | 24 | frontRouter.get('/login', function(req) { 25 | return bogart.html(''); 26 | }); 27 | 28 | var app = bogart.app(); 29 | app.use(bogart.middleware.session({ secret: 'ABC123' })); 30 | app.use(bogart.middleware.directory(path.join(__dirname, 'public'))); 31 | app.use(frontRouter); 32 | app.use(bogart.middleware.facebook(facebookConfig)); 33 | app.use(router); 34 | 35 | app.start(PORT); 36 | -------------------------------------------------------------------------------- /examples/facebook-authentication/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nrstott/bogart/a6e3b01b2e6f1247dde697038101fd6fa94e76b0/examples/facebook-authentication/public/favicon.ico -------------------------------------------------------------------------------- /examples/facebook-authentication/public/login-with-facebook.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nrstott/bogart/a6e3b01b2e6f1247dde697038101fd6fa94e76b0/examples/facebook-authentication/public/login-with-facebook.png -------------------------------------------------------------------------------- /examples/flash-test.js: -------------------------------------------------------------------------------- 1 | var bogart = require('../lib/bogart'); 2 | 3 | var flashConfig = { 4 | encryptionKey: 'db0031e2-04e7-11e1-86ad-000c290196f7' 5 | }; 6 | 7 | var router = bogart.router(); 8 | router.get('/', function (req) { 9 | var html = 'Bogart Flash

Bogart Flash Middleware

'; 10 | html += '

A random number has been inserted into flash. ' + 11 | 'On the first request, the value should be undefined. Subsequent requests should ' + 12 | 'display the random number set on the previous request. ' + 13 | 'Refresh the page to see the flash value change.

' + 14 | 'Existing Flash: '+ req.flash('foo') +'
'; 15 | 16 | req.flash('foo', Math.random() * 10); 17 | 18 | return bogart.html(html); 19 | }); 20 | 21 | var app = bogart.app(); 22 | app.use(bogart.middleware.flash(flashConfig)); 23 | app.use(router); 24 | 25 | app.start(); 26 | -------------------------------------------------------------------------------- /examples/gzip-response.js: -------------------------------------------------------------------------------- 1 | var bogart = require('../lib/bogart'); 2 | 3 | var router = new bogart.router(); 4 | router.get('/', function() { 5 | return bogart.html('

This response is compressed by the Deflate middleware!

'); 6 | }); 7 | 8 | var app = bogart.app(); 9 | app.use(bogart.middleware.gzip); 10 | app.use(router); 11 | 12 | app.start(); -------------------------------------------------------------------------------- /examples/hello-world.js: -------------------------------------------------------------------------------- 1 | var bogart = require('../lib/bogart'); 2 | 3 | var router = bogart.router(); 4 | router.get('/', function(req) { 5 | return 'Hello Root'; 6 | }); 7 | 8 | router.get('/:name', function(req) { 9 | return 'Hello '+req.params.name; 10 | }); 11 | 12 | var app = bogart.app(); 13 | app.use(bogart.batteries({ secret: 'my-secret' })); 14 | app.use(router); 15 | 16 | app.start(); 17 | -------------------------------------------------------------------------------- /examples/multipart-form/app.js: -------------------------------------------------------------------------------- 1 | var bogart = require('../../lib/bogart') 2 | , util = require('util'); 3 | 4 | var router = bogart.router(); 5 | 6 | router.get('/', function(req) { 7 | var html = '' + 8 | 'multi' + 9 | '' + 10 | '
' + 11 | ' ' + 12 | ' ' + 13 | ' ' + 14 | ' ' + 15 | '
'; 16 | 17 | return bogart.html(html); 18 | }); 19 | 20 | router.post('/', function(req) { 21 | console.log(req.body); 22 | 23 | return bogart.text(util.inspect(req.body)); 24 | }); 25 | 26 | var app = bogart.app(); 27 | app.use(bogart.middleware.parted); 28 | app.use(router); 29 | 30 | app.start(); 31 | -------------------------------------------------------------------------------- /examples/mustache-layout/app.js: -------------------------------------------------------------------------------- 1 | var bogart = require('../../lib/bogart') 2 | , path = require('path'); 3 | 4 | var viewEngine = bogart.viewEngine('mustache', path.join(bogart.maindir(), 'views')); 5 | 6 | var router = bogart.router(); 7 | 8 | router.get('/', function(req, res) { 9 | return viewEngine.respond('index.html', { locals: { description: 'This is content' } }); 10 | }); 11 | 12 | var app = bogart.app(); 13 | app.use(router); 14 | 15 | app.start(); 16 | -------------------------------------------------------------------------------- /examples/mustache-layout/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mustache-layout-example", 3 | "version": "1.0.0", 4 | "author": "Nathan Stott", 5 | "email": "nathan.stott@whiteboard-it.com", 6 | "main": "./app.js", 7 | "directories": { "lib": "./lib" }, 8 | "dependencies": { 9 | "promised-io": "v0.2.1", 10 | "jsgi": "v0.2.2", 11 | "mustache": "0.3.1-dev" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /examples/mustache-layout/views/index.html: -------------------------------------------------------------------------------- 1 |

2 | {{ description }} 3 |

4 | -------------------------------------------------------------------------------- /examples/mustache-layout/views/layout.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Mustache with Layout 4 | 5 | 6 |

Hello

7 | {{{ body }}} 8 | 9 | 10 | -------------------------------------------------------------------------------- /examples/pipe-file.js: -------------------------------------------------------------------------------- 1 | var bogart = require('../lib/bogart'); 2 | var fs = require('fs'); 3 | var path = require('path'); 4 | 5 | var router = bogart.router(); 6 | 7 | router.get('/', function(req) { 8 | return bogart.html(''); 9 | }); 10 | 11 | router.get('/image.jpg', function(req) { 12 | var filePath = path.join(__dirname, 'static-server', 'public', 'images', 'ninja-cat.jpg') 13 | , stat = fs.statSync(filePath); 14 | 15 | return bogart.pipe(fs.createReadStream(filePath), { 16 | headers: { 'Content-Type': 'image/jpeg', 'Content-Length': stat.size } 17 | }); 18 | }); 19 | 20 | router.get('/cat.jpg', function(req) { 21 | var filePath = path.join(__dirname, 'static-server', 'public', 'images', 'ninja-cat.jpg'); 22 | 23 | return bogart.file(filePath); 24 | }); 25 | 26 | var app = bogart.app(); 27 | app.use(router); 28 | 29 | app.start(); 30 | -------------------------------------------------------------------------------- /examples/proxy.js: -------------------------------------------------------------------------------- 1 | var bogart = require('../lib/bogart'); 2 | 3 | var router = bogart.router(); 4 | router.get('/', function() { 5 | return bogart.proxy('http://google.com'); 6 | }); 7 | 8 | var app = bogart.app(); 9 | app.use(router); 10 | 11 | app.start(); 12 | -------------------------------------------------------------------------------- /examples/session.js: -------------------------------------------------------------------------------- 1 | var bogart = require('../lib/bogart'); 2 | 3 | var router = bogart.router(); 4 | 5 | router.get('/', function (req) { 6 | var html = 'Bogart Session Demo'; 7 | 8 | html += '
' 9 | html += '

Session Values

'; 10 | html += '
    '; 11 | 12 | req.session.keys().forEach(function (key) { 13 | html += '
  • '+key+': '+req.session(key)+'
  • '; 14 | }); 15 | 16 | html += '
'; 17 | 18 | html += '

Add to Session

'; 19 | html += '
'; 20 | html += ''; 21 | html += ''; 22 | html += '' 23 | html += ''; 24 | html += '' 25 | html += '
'; 26 | html += '

Destroy Session

'; 27 | html += '
'; 28 | html += ''; 29 | html += ''; 30 | html += '
'; 31 | html += '
'; 32 | 33 | return bogart.html(html); 34 | }); 35 | 36 | router.post('/', function (req) { 37 | req.session(req.params.key, req.params.val); 38 | 39 | return bogart.redirect('/'); 40 | }); 41 | 42 | router.del('/destroy', function (req) { 43 | req.session.destroy(); 44 | 45 | return bogart.redirect('/'); 46 | }); 47 | 48 | var sessionConfig = { 49 | lifetime: 600, 50 | secret: 'ABC1234' 51 | }; 52 | 53 | var app = bogart.app(); 54 | app.use(bogart.middleware.parted()); 55 | app.use(bogart.middleware.methodOverride()); 56 | app.use(bogart.middleware.session(sessionConfig)); 57 | app.use(router); 58 | 59 | app.start(1337); 60 | -------------------------------------------------------------------------------- /examples/share-javascript/app.js: -------------------------------------------------------------------------------- 1 | var bogart = require('../../lib/bogart'); 2 | 3 | var viewEngine = bogart.viewEngine('mustache'); 4 | 5 | viewEngine.share(new Date(), 'now'); 6 | 7 | viewEngine.share({ 8 | app: 'JavaScript Sharing Example', 9 | framework: 'Bogart', 10 | author: 'Nathan Stott' 11 | }, 'settings'); 12 | 13 | viewEngine.share(function() { 14 | return 'Shared on the server, executed on the client'; 15 | }, 'fn'); 16 | 17 | var router = bogart.router() 18 | router.get('/', function(req) { 19 | return viewEngine.respond('index.html', { locals: { title: 'Hello World' }, layout: false }); 20 | }); 21 | 22 | var app = bogart.app(); 23 | app.use(router); 24 | app.start(); 25 | -------------------------------------------------------------------------------- /examples/share-javascript/views/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | {{title}} 4 | 5 | 6 |

{{title}}

7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 |
Server Time
Server Object
Calling a Server-Provided Function
20 | 21 | {{{javascript}}} 22 | 23 | 30 | -------------------------------------------------------------------------------- /examples/static-server/app.js: -------------------------------------------------------------------------------- 1 | var 2 | bogart = require("../../lib/bogart"); 3 | 4 | var router = bogart.router(); 5 | 6 | router.get("/", function(req) { 7 | return bogart.html(""); 8 | }); 9 | 10 | var root = require("path").join(__dirname, "public"); 11 | 12 | var app = bogart.app(); 13 | app.use(bogart.middleware.directory(root)); 14 | app.use(router); 15 | 16 | app.start(); 17 | -------------------------------------------------------------------------------- /examples/static-server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "static-server-example", 3 | "version": "1.0.0", 4 | "author": "Nathan Stott", 5 | "email": "nathan.stott@whiteboard-it.com", 6 | "main": "./app.js", 7 | "directories": { "lib": "./lib" }, 8 | "dependencies": { 9 | "promised-io": "v0.2.1", 10 | "jsgi": "v0.2.2", 11 | "mustache": "0.3.1-dev" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /examples/static-server/public/images/ninja-cat.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nrstott/bogart/a6e3b01b2e6f1247dde697038101fd6fa94e76b0/examples/static-server/public/images/ninja-cat.jpg -------------------------------------------------------------------------------- /examples/task-list/app.js: -------------------------------------------------------------------------------- 1 | var bogart = require("../../lib/bogart") 2 | , path = require("path"); 3 | 4 | // Mustache view engine, templates are located in the "task-list/templates" directory 5 | var viewEngine = bogart.viewEngine("mustache", path.join(__dirname, "templates")); 6 | 7 | // Tasks storage, replace with a database in a real application. 8 | var tasks = {}; 9 | 10 | // Most Bogart applications will have a router. Router's generally contain the business logic 11 | // for the application. 12 | var router = bogart.router(); 13 | 14 | router.get("/", function(req) { 15 | var 16 | errors = req.params.errors, 17 | taskList = []; 18 | 19 | // The template needs the tasks as an array, transform the tasks into an array 20 | for (var taskName in tasks) { 21 | taskList.push(tasks[taskName]); 22 | } 23 | 24 | // The index.html file is a mustache template located in the "templates" directory 25 | return viewEngine.respond("index.html", { locals: { tasks: taskList, errors: errors } }); 26 | }); 27 | 28 | router.post("/", function(req) { 29 | var 30 | task = { name: req.params.name, description: req.params.description }, 31 | errors = []; 32 | 33 | if (!task.name || task.name.trim() === "") { 34 | errors.push("name is required"); 35 | } 36 | 37 | // PRG pattern http://en.wikipedia.org/wiki/Post/Redirect/Get 38 | if (errors.length > 0) { 39 | return bogart.redirect("/?errors="+JSON.stringify(errors)); 40 | } 41 | 42 | tasks[task.name] = task; 43 | 44 | return bogart.redirect("/"); 45 | }); 46 | 47 | router.del("/:name", function(req) { 48 | console.log('deleting '+req.params.name); 49 | console.log(tasks); 50 | delete tasks[req.params.name]; 51 | 52 | return bogart.redirect("/"); 53 | }); 54 | 55 | // Create a Bogart Application. Application makes it easy for us to manage our JSGI appliances and routers. 56 | var app = bogart.app(); 57 | 58 | // Frameworks are better when batteries are included. Put the batteries into this app! 59 | app.use(bogart.batteries, { 60 | session: { 61 | secret: "some secret key.." 62 | } 63 | }); 64 | 65 | // Add our router to the app. NOTE: It is important to add batteries first. 66 | app.use(router); 67 | 68 | // Start the server 69 | app.start(); 70 | console.log("Task Server Started!"); 71 | -------------------------------------------------------------------------------- /examples/task-list/templates/index.html: -------------------------------------------------------------------------------- 1 |

Task List

2 | 3 |
    4 | {{#tasks}} 5 |
  • 6 |
    7 | 8 | 9 |
    10 | Task Name: {{name}} 11 |

    Description: {{description}}

    12 |
  • 13 | {{/tasks}} 14 |
15 | 16 |
17 |
18 | Add Task 19 |
20 | 21 | 22 |
23 |
24 | 25 | 26 |
27 |
28 | 29 |
30 |
31 |
32 | -------------------------------------------------------------------------------- /examples/task-list/templates/layout.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | {{{ body }}} 6 | 7 | 8 | -------------------------------------------------------------------------------- /lib/flash/flashCookieDataProvider.js: -------------------------------------------------------------------------------- 1 | var security = require("../security"), 2 | util = require("../util"), 3 | DEFAULT_ENCRYPTION_KEY = "a720efa0-04e8-11e1-a8b5-000c290196f7", 4 | COOKIE_NAME = "jsgi_flash_data"; 5 | 6 | 7 | 8 | module.exports = CookieDataProvider; 9 | 10 | function CookieDataProvider(options) { 11 | this.options = options || {}; 12 | this.flashData = {}; 13 | } 14 | 15 | CookieDataProvider.prototype.previousFlash = function(req, flashId) { 16 | var cookies = util.parseCookies(req); 17 | if (cookies && cookies[COOKIE_NAME]) { 18 | var hold = security.decrypt(decodeURIComponent(cookies[COOKIE_NAME]), this.options.encryptionKey || DEFAULT_ENCRYPTION_KEY); 19 | return hold === "" ? {} : JSON.parse(hold); 20 | } else return null; 21 | }; 22 | 23 | CookieDataProvider.prototype.setter = function(req, flashId) { 24 | this.flashData[flashId] = {}; 25 | var self = this; 26 | return function(data) { 27 | Object.keys(data).forEach(function(key) { 28 | self.flashData[flashId][key] = data[key]; 29 | }); 30 | }; 31 | }; 32 | 33 | CookieDataProvider.prototype.finalize = function(req, res, flashId) { 34 | res = res || {}; 35 | res.headers = res.headers || {}; 36 | res.headers["Set-Cookie"] = res.headers["Set-Cookie"] || []; 37 | var data = this.flashData[flashId]; 38 | delete this.flashData[flashId]; 39 | 40 | var cookie = COOKIE_NAME + "=" + encodeURIComponent(security.encrypt(JSON.stringify(data), this.options.encryptionKey || DEFAULT_ENCRYPTION_KEY)) + ";Path=/;"; 41 | 42 | if (Object.prototype.toString.call(res.headers["Set-Cookie"]) === "[object String]") { 43 | var existing = res.headers["Set-Cookie"]; 44 | res.headers["Set-Cookie"] = []; 45 | res.headers["Set-Cookie"].push(existing); 46 | } 47 | res.headers["Set-Cookie"].push(cookie); 48 | 49 | return res; 50 | }; 51 | 52 | 53 | CookieDataProvider.prototype.clear = function(req, res, flashId) { 54 | res = res || {}; 55 | res.headers = res.headers || {}; 56 | res.headers["Set-Cookie"] = res.headers["Set-Cookie"] || []; 57 | 58 | var cookies = util.parseCookies(req); 59 | if (cookies && cookies[COOKIE_NAME]) { 60 | var yesterday = new Date(); 61 | yesterday.setDate(yesterday.getDate() - 1); 62 | var clearCookie = COOKIE_NAME + "=expiring; Expires=" + yesterday.toUTCString() + ";Path=/;"; 63 | 64 | if (Object.prototype.toString.call(res.headers["Set-Cookie"]) === "[object String]") { 65 | var existing = res.headers["Set-Cookie"]; 66 | res.headers["Set-Cookie"] = []; 67 | res.headers["Set-Cookie"].push(existing); 68 | } 69 | 70 | res.headers["Set-Cookie"] = res.headers["Set-Cookie"].filter(function(el) { 71 | return !(new RegExp("^" + COOKIE_NAME + "=").test(el)); 72 | }); 73 | res.headers["Set-Cookie"].push(clearCookie); 74 | } 75 | 76 | return res; 77 | }; -------------------------------------------------------------------------------- /lib/flash/flashIdCookieProvider.js: -------------------------------------------------------------------------------- 1 | var uuid = require("node-uuid"), 2 | security = require("../security"), 3 | util = require("../util"), 4 | flashCookieName = "jsgi_flash_session"; 5 | 6 | module.exports = CookieIdProvider; 7 | 8 | function CookieIdProvider(options) { 9 | this.options = options || {}; 10 | } 11 | 12 | CookieIdProvider.prototype.getFlashId = function(req) { 13 | var cookies = util.parseCookies(req); 14 | if (cookies && cookies[flashCookieName]) { 15 | return cookies[flashCookieName]; 16 | } else return null; 17 | }; 18 | 19 | CookieIdProvider.prototype.newId = function(req) { 20 | return uuid(); 21 | }; 22 | 23 | CookieIdProvider.prototype.finalize = function(req, res, flashId) { 24 | res = res || {}; 25 | res.headers = res.headers || {}; 26 | res.headers["Set-Cookie"] = res.headers["Set-Cookie"] || []; 27 | var cookie = flashCookieName + "=" + flashId + ";Path=/;"; 28 | 29 | if (Object.prototype.toString.call(res.headers["Set-Cookie"]) === "[object String]") { 30 | var existing = res.headers["Set-Cookie"]; 31 | res.headers["Set-Cookie"] = []; 32 | res.headers["Set-Cookie"].push(existing); 33 | } 34 | 35 | res.headers["Set-Cookie"] = res.headers["Set-Cookie"].filter(function(el) { 36 | return !(new RegExp("^" + flashCookieName + "=").test(el)); 37 | }); 38 | res.headers["Set-Cookie"].push(cookie); 39 | 40 | return res; 41 | }; 42 | 43 | CookieIdProvider.prototype.clear = function(req, res, flashId) { 44 | res = res || {}; 45 | res.headers = res.headers || {}; 46 | res.headers["Set-Cookie"] = res.headers["Set-Cookie"] || []; 47 | 48 | var cookies = util.parseCookies(req); 49 | if (cookies && cookies[flashCookieName]) { 50 | var yesterday = new Date(); 51 | yesterday.setDate(yesterday.getDate() - 1); 52 | var clearCookie = flashCookieName + "=expiring; Expires=" + yesterday.toUTCString() + ";Path=/;"; 53 | 54 | if (Object.prototype.toString.call(res.headers["Set-Cookie"]) === "[object String]") { 55 | var existing = res.headers["Set-Cookie"]; 56 | res.headers["Set-Cookie"] = []; 57 | res.header["Set-Cookie"].push(existing); 58 | } 59 | 60 | res.headers["Set-Cookie"] = res.headers["Set-Cookie"].filter(function(el) { 61 | return !(new RegExp("^" + flashCookieName + "=").test(el)); 62 | }) || []; 63 | res.headers["Set-Cookie"].push(clearCookie); 64 | } 65 | 66 | return res; 67 | }; 68 | -------------------------------------------------------------------------------- /lib/forEachStream.js: -------------------------------------------------------------------------------- 1 | var util = require('util') 2 | , Readable = require('stream').Readable 3 | , Q = require('./q'); 4 | 5 | var enqueue = typeof setImmediate === 'function' ? setImmediate : process.nextTick; 6 | 7 | function ForEachStream(forEachable) { 8 | if (!(this instanceof ForEachStream)) return new ForEachStream(forEachable); 9 | 10 | Readable.call(this); 11 | 12 | var self = this; 13 | 14 | this._waitingDeferred = null; 15 | this._readingDeferred = null; 16 | 17 | this._read = function () { 18 | if (this._waitingDeferred !== null) { 19 | this._waitingDeferred.resolve(); 20 | this._waitingDeferred = null; 21 | return; 22 | } 23 | 24 | if (this._readingDeferred !== null) { 25 | return; 26 | } 27 | 28 | this._readingDeferred = Q.when(forEachable.forEach(function (data) { 29 | if (self.push(data) === false) { 30 | self._waitingDeferred = Q.defer(); 31 | return self._waitingDeferred.promise; 32 | } 33 | }), function () { 34 | self.push(null); 35 | }, function (err) { 36 | self.emit('error', err); 37 | }); 38 | }; 39 | }; 40 | 41 | require('util').inherits(ForEachStream, Readable); 42 | 43 | module.exports = ForEachStream; 44 | -------------------------------------------------------------------------------- /lib/forEachable.js: -------------------------------------------------------------------------------- 1 | var q = require('./q'); 2 | 3 | exports.join = function (forEachable) { 4 | var body = ''; 5 | 6 | function appendChunk(chunk) { 7 | body += chunk; 8 | }; 9 | 10 | return q.when(forEachable.forEach(appendChunk), function () { 11 | return body; 12 | }); 13 | }; -------------------------------------------------------------------------------- /lib/fsp.js: -------------------------------------------------------------------------------- 1 | // Promise-based file-system functions 2 | 3 | var fs = require('fs') 4 | , q = require('./q'); 5 | 6 | exports.readFile = function (filePath) { 7 | var deferred = q.defer(); 8 | 9 | fs.readFile(filePath, function(err, data) { 10 | if (err) { 11 | deferred.reject(err); 12 | } else { 13 | deferred.resolve(data); 14 | } 15 | }); 16 | 17 | return deferred.promise; 18 | } 19 | 20 | exports.stat = function (fileOrFolderPath) { 21 | var deferred = q.defer(); 22 | 23 | fs.stat(fileOrFolderPath, function(err, status) { 24 | if (err) { 25 | deferred.reject(err); 26 | } else { 27 | deferred.resolve(status); 28 | } 29 | }); 30 | 31 | return deferred.promise; 32 | } -------------------------------------------------------------------------------- /lib/middleware.js: -------------------------------------------------------------------------------- 1 | var Q = require('./q'), 2 | when = Q.when, 3 | fs = require('fs'), 4 | path = require('path'), 5 | ForEachStream = require('./forEachStream'), 6 | util = require('./util'), 7 | merge = util.merge, 8 | EventEmitter = require('events').EventEmitter, 9 | _ = require('underscore'), 10 | Router = require('./router').Router; 11 | 12 | var middleware = function (handler) { 13 | return function factory(next) { 14 | if (next === undefined) { 15 | return function (next) { 16 | return factory(next); 17 | }; 18 | } 19 | 20 | return function (req) { 21 | return handler(req, next); 22 | } 23 | } 24 | }; 25 | 26 | module.exports = middleware; 27 | 28 | function deprecate(oldName, newName, fn) { 29 | return function () { 30 | console.log(oldName + ' is deprecated, please use ' + newName + ' instead'); 31 | return fn.apply(middleware, Array.prototype.slice.call(arguments)); 32 | } 33 | } 34 | 35 | middleware.freshEnv = middleware(function (req, next) { 36 | req.env = _.extend({}, req.env); 37 | 38 | return next(req); 39 | }); 40 | 41 | middleware.parseJson = require('./middleware/parseJson'); 42 | middleware.ParseJson = deprecate('ParseJson', 'parseJson', middleware.parseJson); 43 | 44 | middleware.parseForm = require('./middleware/parseForm'); 45 | middleware.ParseForm = deprecate('ParseForm', 'parseForm', middleware.parseForm); 46 | 47 | middleware.gzip = require('./middleware/gzip'); 48 | middleware.Gzip = deprecate('Gzip', 'gzip', middleware.gzip); 49 | 50 | /** 51 | * Provides Rails-style HTTP method overriding via the _method parameter or X-HTTP-METHOD-OVERRIDE header 52 | * http://code.google.com/apis/gdata/docs/2.0/basics.html#UpdatingEntry 53 | */ 54 | middleware.methodOverride = require('./middleware/methodOverride'); 55 | middleware.MethodOverride = deprecate('MethodOverride', 'methodOverride', middleware.methodOverride); 56 | 57 | middleware.parted = require('./middleware/parted'); 58 | middleware.Parted = deprecate('Parted', 'parted', middleware.parted); 59 | 60 | /** 61 | * Translates rejected promises to a JSGI error response. 62 | * 63 | * @param errorResponse {Function} Optional. A function that returns a JSGI response when passed an error. 64 | * @returns {Function} JSGI response. 65 | */ 66 | middleware.error = require('./middleware/error'); 67 | middleware.Error = deprecate('Error', 'error', middleware.error); 68 | 69 | /** 70 | * Validates that the response from nextApp is a valid JSGI response. 71 | * Rejects the promise if the response is invalid. 72 | * 73 | * @param {Function} nextApp The next application in the JSGI chain. 74 | * @returns {Function} JSGI response. 75 | */ 76 | middleware.validateResponse = require('./middleware/validateResponse'); 77 | middleware.ValidateResponse = deprecate('ValidateResponse', 'validateResponse', middleware.validateResponse); 78 | 79 | middleware.directory = require('./middleware/directory'); 80 | middleware.Directory = deprecate('Directory', 'directory', middleware.directory); 81 | 82 | middleware.flash = require('./middleware/flash'); 83 | middleware.Flash = deprecate('Flash', 'flash', middleware.flash); 84 | 85 | /** 86 | * Creates a OAuth2 section to auth routes requiring oauth2 87 | * 88 | */ 89 | middleware.oauth = require('./middleware/oauth'); 90 | 91 | middleware.oauth2 = require('./middleware/oauth2'); 92 | 93 | middleware.facebook = require('./middleware/facebook'); 94 | 95 | middleware.google = require('./middleware/google'); 96 | 97 | middleware.twitter = require('./middleware/twitter'); 98 | 99 | middleware.session = middleware.Session = require("./middleware/session/session").Session; 100 | 101 | /** 102 | * Adapts Node.JS Buffer and Stream types used as response bodies into 103 | * a CommonJS ForEachable. 104 | * 105 | * @param {Function} nextApp The next application in the JSGI chain. 106 | * @returns {Function} JSGI Application. 107 | */ 108 | middleware.bodyAdapter = require('./middleware/bodyAdapter'); 109 | 110 | /** 111 | * Translates strings into JSGI response objects. 112 | * 113 | * The default response has a status of 200 and a "Content-Type" header of "text/html" 114 | * 115 | * @param {Object} responseDefaults Optional parameter. If stringReturnAdapter is called with one argument, that argument is assumed to be nextApp, not responseDefaults. 116 | * @param {Function} nextApp Next application in the JSGI chain. 117 | * 118 | * @returns {Function} JSGI Application. 119 | */ 120 | middleware.stringReturnAdapter = require('./middleware/stringReturnAdapter'); 121 | 122 | /** 123 | * JSGI Middleware that tries a series of JSGI Applications in order until one succeeds the `accept` test. 124 | * The first parameter is expected to be a function that will be called with the values returned by the applications. 125 | * The `accept` function must return a boolean indicating whether this response is the one that should be returned or 126 | * the next application in the series should be called. 127 | * 128 | * var cascade = bogart.middleware.cascade(function(res) { return res.status !== 404 }, 129 | * function(req) { 130 | * return { 131 | * status: 404, 132 | * headers: {} 133 | * body: [] 134 | * }; 135 | * }, function(req) { 136 | * return { 137 | * status: 200, 138 | * headers: { 'Content-Type': 'text/html' }, 139 | * body: [ 'Hello World' ] 140 | * }; 141 | * }); 142 | * cascade(req); // The 2nd middleware function's return would be accepted and would become the return from cascade. 143 | * 144 | * @params {Function} accept A function `function(res) {}` that takes a response and returns a boolean. 145 | * @params {Function} apps The rest of the arguments are used as applications to cascade through. 146 | * 147 | * @returns {Promise} JSGI Response 148 | */ 149 | middleware.cascade = require('./middleware/cascade'); 150 | 151 | /* 152 | * Configuration factory for `bogart.batteries`. 153 | * 154 | * @param {Object} overrides Optional prameter providing overrides 155 | * for default configuration options. 156 | * 157 | * @api private 158 | */ 159 | function batteriesConfig(overrides) { 160 | overrides = overrides || {}; 161 | 162 | return _.extend({}, batteriesConfig.default, overrides); 163 | }; 164 | 165 | batteriesConfig.default = { 166 | directory: 'public', 167 | error: {}, 168 | session: {} 169 | }; 170 | 171 | /** 172 | * All the goodies in one convenient middleware. 173 | * 174 | * Includes the following JSGI Chain: 175 | * 176 | * error -> validateResponse -> gzip -> directory -> parted -> methodOverride 177 | * -> session -> flash -> bodyAdapter -> stringReturnAdapter -> nextApp 178 | * 179 | * @param {Object} config Optional configuration, if arity is two first parameter is config. 180 | * @param {Function} nextApp The next application (Middleware, Bogart Router, etc...) in the JSGI chain. 181 | * @returns {Function} A good default middleware stack in one function call. 182 | */ 183 | middleware.batteries = function(config, nextApp) { 184 | if (nextApp === undefined) { 185 | if (typeof config === 'function') { 186 | nextApp = config; 187 | 188 | config = null; 189 | } else { 190 | return function (nextApp) { 191 | return middleware.batteries(config, nextApp); 192 | }; 193 | } 194 | } 195 | 196 | config = batteriesConfig(config); 197 | if (config.secret) { 198 | config.session.secret = config.secret; 199 | } 200 | 201 | if (!nextApp) { 202 | throw 'Bogart batteries requires at least one parameter, a nextApp to execute to fulfill the request.' 203 | } 204 | 205 | var stack = middleware.freshEnv( 206 | middleware.error(config.error, 207 | middleware.gzip( 208 | middleware.directory(config.directory, 209 | middleware.parted(config.parted || undefined, 210 | middleware.methodOverride( 211 | middleware.session(config.session, 212 | middleware.flash(config.flash || undefined, 213 | middleware.bodyAdapter( 214 | middleware.stringReturnAdapter(nextApp)))))))))); 215 | 216 | return stack; 217 | }; 218 | -------------------------------------------------------------------------------- /lib/middleware/bodyAdapter.js: -------------------------------------------------------------------------------- 1 | var q = require('../q') 2 | , middleware = require('../middleware'); 3 | 4 | module.exports = middleware(function (req, nextApp) { 5 | return q.when(nextApp(req), function (resp) { 6 | if (resp.body && resp.body.forEach) { 7 | return resp; 8 | } 9 | 10 | if (Buffer.isBuffer(resp)) { 11 | return { 12 | status: 200, 13 | headers: { 'content-type': 'text/html' }, 14 | body: [ resp.toString('utf-8') ] 15 | }; 16 | } 17 | 18 | if (Buffer.isBuffer(resp.body)) { 19 | resp.body = [ resp.body.toString('utf-8') ]; 20 | return resp; 21 | } 22 | 23 | var searchFriendlyErrorCode = 'BODY_ADAPTER_BAD_RESPONSE_BODY'; 24 | var reason = 'Middleware error: The response object returned by your route handler for "'+req.pathInfo+'" is not a proper bogart JSGI response. It does not have a forEach method, is not a Buffer, and is not a readable Stream'; 25 | 26 | throw new Error(reason + ' ' + searchFriendlyErrorCode); 27 | }); 28 | }); 29 | -------------------------------------------------------------------------------- /lib/middleware/cascade.js: -------------------------------------------------------------------------------- 1 | module.exports = function() { 2 | throw new Error('Cascade middleware has been removed. \ 3 | If you had a use-case for it that can no be accomplished via bogart.app, \ 4 | please share and will consider readding it.'); 5 | }; -------------------------------------------------------------------------------- /lib/middleware/directory.js: -------------------------------------------------------------------------------- 1 | var fsp = require('../fsp') 2 | , q = require('../q') 3 | , qfs = require('q-io/fs') 4 | , mimeType = require('../mimetypes').mimeType 5 | , path = require('path') 6 | , _ = require('underscore') 7 | , notFoundMessage = "404 Not Found" 8 | , nfLength = Buffer.byteLength(notFoundMessage) 9 | , response404 = { 10 | status: 404, 11 | body: [ notFoundMessage ], 12 | headers: { 13 | "content-length": nfLength 14 | } 15 | } 16 | ; 17 | 18 | function respondWithFile(req, root, filePath, contentType, headers) { 19 | if (!qfs.contains(root, filePath)) { 20 | return response404; 21 | } 22 | return q.when(fsp.stat(filePath), function (stat) { 23 | var etag, indexPath; 24 | 25 | if (!stat.isFile()) { 26 | if (stat.isDirectory()) { 27 | indexPath = path.join(filePath, "index.html"); 28 | return respondWithFile(req, root, indexPath, contentType, headers); 29 | } else { 30 | return response404; 31 | } 32 | } 33 | 34 | headers = headers || {}; 35 | 36 | if (typeof contentType === 'object') { 37 | headers = contentType; 38 | contentType = undefined; 39 | } 40 | 41 | contentType = mimeType(path.extname(filePath), contentType); 42 | 43 | etag = [stat.ino, stat.size, Date.parse(stat.mtime)].join("-"); 44 | 45 | if (req.headers && req.headers["if-none-match"] === etag) { 46 | return { 47 | status: 304, 48 | body: [], 49 | headers: {} 50 | }; 51 | } 52 | 53 | return q.when(fsp.readFile(filePath), function onSuccess(contents) { 54 | return { 55 | status: 200, 56 | headers: _.extend({ 57 | etag: etag, 58 | "content-type": contentType 59 | }, headers), 60 | body: [contents] 61 | } 62 | }); 63 | }, function(err) { 64 | if (err && err.code === 'ENOENT') { 65 | return response404; 66 | } else { 67 | throw err; 68 | } 69 | }); 70 | }; 71 | 72 | module.exports = function directory(root, nextApp) { 73 | var opts; 74 | 75 | if (nextApp === undefined) { 76 | if (typeof root === 'function') { 77 | nextApp = root; 78 | root = undefined; 79 | } else { 80 | return function (nextApp) { 81 | return directory(root, nextApp); 82 | }; 83 | } 84 | } 85 | 86 | if (typeof root === 'string') { 87 | opts = { root: root }; 88 | } else if (typeof root === 'object') { 89 | opts = root; 90 | } else { 91 | opts = {}; 92 | } 93 | 94 | opts.headers = opts.headers || {}; 95 | 96 | return function (req) { 97 | var reqPath = path.join(opts.root, req.pathInfo.substring(1)); 98 | 99 | return q.when(respondWithFile(req, root, reqPath, opts.headers), function(res) { 100 | if (res.status === 404 && nextApp) { 101 | return nextApp(req); 102 | } else { 103 | return res; 104 | } 105 | }); 106 | }; 107 | }; 108 | -------------------------------------------------------------------------------- /lib/middleware/error.js: -------------------------------------------------------------------------------- 1 | var q = require('../q') 2 | , _ = require('underscore'); 3 | 4 | function errorResponse(config, req, err) { 5 | var message = '

Message:

'; 6 | 7 | if (config.showError) { 8 | if (typeof err === 'string') { 9 | message += err; 10 | } else if (err.message) { 11 | message += err.message; 12 | if (err.stack) { 13 | message += '

Stack Trace:

'; 14 | message += '
'; 15 | if(err.stack.join) { 16 | message += err.stack.join('
'); 17 | } else { 18 | message += err.stack.replace(/\r?\n/g, '
'); 19 | } 20 | message += '
'; 21 | } 22 | } 23 | } else { 24 | message = ''; 25 | } 26 | 27 | if (config.logError) { 28 | console.error('Error processing request', req.pathInfo, err); 29 | } 30 | 31 | return { 32 | status: 500, 33 | body: ['Error

An error occurred.

', message, ''], 34 | headers: { 35 | 'content-type': 'text/html' 36 | } 37 | }; 38 | }; 39 | 40 | var defaultConfig = { 41 | showError: true, 42 | logError: true 43 | }; 44 | 45 | /** 46 | * Translates rejected promises to a JSGI error response. 47 | * 48 | * @param errorResponse {Function} Optional. A function that returns a JSGI response when passed an error. 49 | * @returns {Function} JSGI response. 50 | */ 51 | module.exports = function errorMiddleware(config, nextApp) { 52 | if (nextApp === undefined) { 53 | if (typeof config === 'function') { 54 | nextApp = config; 55 | config = {}; 56 | } else { 57 | return function (nextApp) { 58 | return errorMiddleware(config, nextApp); 59 | }; 60 | } 61 | } 62 | 63 | config = _.extend({}, defaultConfig, config); 64 | 65 | return function (req) { 66 | return q.whenCall(function() { return nextApp(req); }, function(val) { 67 | return val; 68 | }, function (err) { 69 | return errorResponse(config, req, err); 70 | }); 71 | }; 72 | }; 73 | -------------------------------------------------------------------------------- /lib/middleware/facebook.js: -------------------------------------------------------------------------------- 1 | var middleware = require('../middleware') 2 | , _ = require('underscore'); 3 | 4 | function parseFacebookProfile(body) { 5 | if (body){ 6 | var o = JSON.parse(body); 7 | var profile = { 8 | provider: 'facebook' 9 | }; 10 | profile.id = o.id; 11 | profile.username = o.username; 12 | profile.displayName = o.name; 13 | profile.name = { 14 | familyName: o.last_name, 15 | givenName: o.first_name, 16 | middleName: o.middle_name 17 | }; 18 | profile.gender = o.gender; 19 | profile.profileUrl = o.link; 20 | profile.emails = [ o.email ]; 21 | return profile; 22 | } 23 | return undefined; 24 | } 25 | 26 | module.exports = facebookMiddleware; 27 | 28 | function facebookMiddleware(config, nextApp) { 29 | if (nextApp === undefined) { 30 | return function (nextApp) { 31 | return facebookMiddleware(config, nextApp); 32 | }; 33 | } 34 | 35 | var facebook_strategy = { 36 | authorizationURL: 'https://www.facebook.com/dialog/oauth', 37 | tokenURL: 'https://graph.facebook.com/oauth/access_token', 38 | resourceURL: 'https://graph.facebook.com/me', 39 | parse: facebookMiddleware.parseFacebookProfile 40 | }; 41 | 42 | var oauth2Options = _.extend(facebook_strategy, config); 43 | 44 | return middleware.oauth2(oauth2Options, nextApp); 45 | }; 46 | 47 | facebookMiddleware.parseFacebookProfile = parseFacebookProfile; 48 | 49 | -------------------------------------------------------------------------------- /lib/middleware/flash.js: -------------------------------------------------------------------------------- 1 | var q = require('../q') 2 | , when = q.when 3 | , flashIdCookieProvider = require("../flash/flashIdCookieProvider") 4 | , flashCookieDataProvider = require("../flash/flashCookieDataProvider"); 5 | 6 | module.exports = function flash(config, nextApp) { 7 | if (nextApp === undefined) { 8 | if (typeof config === 'function') { 9 | nextApp = config; 10 | config = {}; 11 | } else { 12 | return function (nextApp) { 13 | return flash(config, nextApp); 14 | }; 15 | } 16 | } 17 | 18 | if (typeof nextApp !== 'function') { 19 | throw { 20 | message: 'flash middleware expects a nextApp as the last argument.', 21 | code: 'BOGART_FLASH_BAD_NEXTAPP' 22 | }; 23 | } 24 | 25 | config = config || {}; 26 | config.options = config.options || {}; 27 | 28 | config.flashIdProvider = config.flashIdProvider || new flashIdCookieProvider(config.options.idProvider || {}); 29 | config.flashDataProvider = config.flashDataProvider || new flashCookieDataProvider(config.options.dataProvider || {}); 30 | 31 | return function (req) { 32 | if (req.pathInfo === '/favicon.ico') { 33 | return nextApp(req); 34 | } 35 | var oldFlashId = config.flashIdProvider.getFlashId(req); 36 | 37 | // get a new id for the current request 38 | var newFlashId = config.flashIdProvider.newId(req); 39 | 40 | // make flash data from previous request available 41 | req.env = req.env || {}; 42 | var prevData = oldFlashId ? config.flashDataProvider.previousFlash(req, oldFlashId) : {}; 43 | 44 | return when(prevData, function (data) { 45 | req.env.flash = data || {}; 46 | // create the setter for new flash data 47 | var setter = config.flashDataProvider.setter(req, newFlashId); 48 | req.flash = function (key, val) { 49 | if (key && val) { 50 | var obj = {}; 51 | obj[key] = val; 52 | setter(obj); 53 | } else return req.env.flash[key]; 54 | } 55 | 56 | return when(nextApp(req), function (resp) { 57 | if (oldFlashId) { 58 | // clear old flash data 59 | resp = config.flashDataProvider.clear(req, resp, oldFlashId); 60 | // clear old flash id 61 | resp = config.flashIdProvider.clear(req, resp, oldFlashId); 62 | } 63 | 64 | // finalize the flashId provider for the current request 65 | resp = config.flashIdProvider.finalize(req, resp, newFlashId); 66 | // finalize the flash data provider for the current request 67 | resp = config.flashDataProvider.finalize(req, resp, newFlashId); 68 | 69 | return resp; 70 | }); 71 | }); 72 | }; 73 | }; -------------------------------------------------------------------------------- /lib/middleware/google.js: -------------------------------------------------------------------------------- 1 | var middleware = require('../middleware') 2 | , _ = require('underscore'); 3 | 4 | function parseGoogleProfile(body) { 5 | if (body) { 6 | o = JSON.parse(body); 7 | var profile = { 8 | provider: 'google' 9 | }; 10 | profile.id = o.id; 11 | profile.displayName = o.displayName; 12 | profile.name = o.name; 13 | profile.gender = o.gender; 14 | profile.profileUrl = o.url; 15 | if (o.emails) { 16 | profile.emails = o.emails; 17 | } 18 | return profile; 19 | } 20 | } 21 | 22 | module.exports = googleMiddleware; 23 | 24 | function googleMiddleware(config, nextApp) { 25 | if (nextApp === undefined) { 26 | return function (nextApp) { 27 | return googleMiddleware(config, nextApp); 28 | }; 29 | } 30 | 31 | var googleStrategy = { 32 | authorizationURL: 'https://accounts.google.com/o/oauth2/auth', 33 | tokenURL: 'https://accounts.google.com/o/oauth2/token', 34 | resourceURL: 'https://www.googleapis.com/plus/v1/people/me', 35 | authorizationParams: { 36 | scope: 'https://www.googleapis.com/auth/plus.profile.emails.read' 37 | }, 38 | parse: googleMiddleware.parseGoogleProfile 39 | }; 40 | 41 | var oauth2Options = _.extend(googleStrategy, config); 42 | 43 | return middleware.oauth2(oauth2Options, nextApp); 44 | }; 45 | 46 | googleMiddleware.parseGoogleProfile = parseGoogleProfile; 47 | -------------------------------------------------------------------------------- /lib/middleware/gzip.js: -------------------------------------------------------------------------------- 1 | var zlib = require('zlib') 2 | , bogart = require('../bogart') 3 | , middleware = require('../middleware') 4 | , q = require('../q'); 5 | 6 | function acceptsGzip(req) { 7 | var encodings; 8 | 9 | if (!req.headers || !req.headers['accept-encoding']) { 10 | return false; 11 | } 12 | 13 | encodings = req.headers['accept-encoding'].split(','); 14 | return encodings.filter(function(x) { 15 | return x === 'gzip'; 16 | }).length > 0; 17 | } 18 | 19 | function toForEachable(stream) { 20 | var forEachable = {} 21 | , deferred = q.defer() 22 | , buffer = [] 23 | , cb = null; 24 | 25 | forEachable.forEach = function(callback) { 26 | cb = callback; 27 | 28 | while (x = buffer.pop()) { 29 | cb(x); 30 | } 31 | 32 | return deferred.promise; 33 | }; 34 | 35 | stream.on('data', function(data) { 36 | if (cb === null) { buffer.push(data); } 37 | else { cb(data); } 38 | }); 39 | 40 | stream.on('end', function() { 41 | deferred.resolve(); 42 | }); 43 | 44 | return forEachable; 45 | } 46 | 47 | module.exports = middleware(function (req, nextApp) { 48 | var resp = nextApp(req); 49 | 50 | if (acceptsGzip(req)) { 51 | return q.when(resp, function(resp) { 52 | var gzip = zlib.createGzip(); 53 | resp.headers['content-encoding'] = 'gzip'; 54 | if (resp.headers['content-length']) { 55 | delete resp.headers['content-length']; 56 | } 57 | if (resp.headers['Content-Length']) { 58 | delete resp.headers['Content-Length']; 59 | } 60 | 61 | bogart.pump(resp.body, gzip); 62 | 63 | if(resp.status != 304) { 64 | resp.body = toForEachable(gzip); 65 | } 66 | return resp; 67 | }); 68 | } else { 69 | return resp; 70 | } 71 | }); 72 | -------------------------------------------------------------------------------- /lib/middleware/methodOverride.js: -------------------------------------------------------------------------------- 1 | var middleware = require('../middleware'); 2 | 3 | var HTTP_METHODS = { 4 | "GET": true, 5 | "HEAD": true, 6 | "PUT": true, 7 | "POST": true, 8 | "DELETE": true, 9 | "OPTIONS": true 10 | }; 11 | 12 | var METHOD_OVERRIDE_PARAM_KEY = "_method"; 13 | var HTTP_METHOD_OVERRIDE_HEADER = 'x-http-method-override'; 14 | 15 | /** 16 | * Provides Rails-style HTTP method overriding via the _method parameter or X-HTTP-METHOD-OVERRIDE header 17 | * http://code.google.com/apis/gdata/docs/2.0/basics.html#UpdatingEntry 18 | */ 19 | var methodOverride = middleware(function (req, nextApp) { 20 | 21 | if (req.body && typeof req.body == 'object') { 22 | 23 | if ((req.method.toUpperCase() == 'POST') && (req.headers['content-type'] && !req.headers['content-type'].match(/^multipart\/form-data/))) { 24 | var method = req.headers[HTTP_METHOD_OVERRIDE_HEADER] || req.body[METHOD_OVERRIDE_PARAM_KEY]; 25 | 26 | if (method && HTTP_METHODS[method.toUpperCase()] === true) { 27 | req.env.original_method = req.method; 28 | req.method = method.toUpperCase(); 29 | } 30 | } 31 | } 32 | return nextApp(req); 33 | }); 34 | 35 | methodOverride.METHOD_OVERRIDE_PARAM_KEY = METHOD_OVERRIDE_PARAM_KEY; 36 | methodOverride.HTTP_METHOD_OVERRIDE_HEADER = HTTP_METHOD_OVERRIDE_HEADER; 37 | 38 | module.exports = methodOverride; 39 | -------------------------------------------------------------------------------- /lib/middleware/oauth.js: -------------------------------------------------------------------------------- 1 | var q = require('../q') 2 | , oauth = require('oauth') 3 | , bogart = require('../bogart') 4 | , _ = require('underscore') 5 | , url = require('url'); 6 | 7 | module.exports = function oauthMiddleware(config, nextApp) { 8 | if (nextApp === undefined) { 9 | return function (nextApp) { 10 | return oauthMiddleware(config, nextApp); 11 | }; 12 | } 13 | 14 | var options = { 15 | loginRoute: '/auth/login', 16 | logoutRoute: '/auth/logout', 17 | callbackRoute: '/auth/callback', 18 | errorUrl: '/' 19 | }; 20 | options = _.extend(options, config); 21 | 22 | if (!options.host) { 23 | throw new Error('host is a required option') 24 | } 25 | 26 | if (!options.host.match(/^http/)) { 27 | throw new Error('host must include the protocal.'); 28 | } 29 | 30 | options.host = options.host.replace(/\/$/, ''); //remove trailing slash 31 | 32 | var OAuth = new oauth.OAuth(options.requestTokenURL, options.accessTokenURL, options.consumerKey, options.consumerSecret, '1.0A', null, 'HMAC-SHA1'); 33 | 34 | var authorized = function(req) { 35 | if (!req.session('profile')) { 36 | return bogart.redirect(options.loginRoute + '?returnUrl=' + req.pathInfo ); 37 | } 38 | req.auth = req.auth || {}; 39 | req.auth.profile = req.session('profile'); 40 | req.auth.access_token = req.session('access_token'); 41 | return nextApp(req); 42 | }; 43 | 44 | var router = bogart.router(authorized); 45 | 46 | router.get(options.loginRoute, function (req) { 47 | var deferred = q.defer(); 48 | var callbackRoute = options.host + options.callbackRoute; 49 | if (req.params.returnUrl) { 50 | callbackRoute += '?returnUrl=' + encodeURIComponent(req.params.returnUrl); 51 | } 52 | var params = { 53 | oauth_callback: callbackRoute 54 | }; 55 | OAuth.getOAuthRequestToken(params, function (error, token, tokenSecret, params) { 56 | if (error) { 57 | deferred.reject(error); 58 | } 59 | req.session('oauth_token_secret', tokenSecret); 60 | var parsed = url.parse(options.authorizationURL, true); 61 | parsed.query.oauth_token = token; 62 | var location = url.format(parsed); 63 | deferred.resolve(bogart.redirect(location)); 64 | }); 65 | return deferred.promise; 66 | }); 67 | 68 | router.get(options.logoutRoute, function (req) { 69 | req.session('profile', undefined); 70 | 71 | return bogart.redirect('/'); 72 | }); 73 | 74 | router.get(options.callbackRoute, function (req) { 75 | var deferred = q.defer(); 76 | var oAuthToken = req.params.oauth_token, 77 | oAuthVerifier = req.params.oauth_verifier, 78 | oAuthTokenSecret = req.session('oauth_token_secret'); 79 | OAuth.getOAuthAccessToken(oAuthToken, oAuthTokenSecret, oAuthVerifier, function (error, accessToken, tokenSecret, params) { 80 | if (error) { 81 | deferred.reject(error); 82 | } 83 | var parsed = url.parse(options.resourceURL, true); 84 | options.resourceURLParams.forEach(function (resourceUrlParam) { 85 | if (params[resourceUrlParam]) { 86 | parsed.query[resourceUrlParam] = params[resourceUrlParam]; 87 | } 88 | }); 89 | var location = url.format(parsed); 90 | OAuth.getProtectedResource(location, 'get', accessToken, tokenSecret, function (error, body, res) { 91 | if (error) { 92 | deferred.reject(error); 93 | } 94 | var profile = q(options.parse(body, req)).then(function (profile) { 95 | req.session('profile', profile); 96 | req.session('access_token', accessToken); 97 | return bogart.redirect(options.successUrl || req.params.returnUrl || '/'); 98 | }).fail(function (error) { 99 | var parsedUrl = url.parse(options.errorUrl, true); 100 | parsedUrl.query.message = error.message; 101 | var redirectUrl = url.format(parsedUrl); 102 | return bogart.redirect(redirectUrl); 103 | }); 104 | deferred.resolve(profile); 105 | }); 106 | }); 107 | return deferred.promise; 108 | }); 109 | 110 | return router(nextApp); 111 | }; 112 | -------------------------------------------------------------------------------- /lib/middleware/oauth2.js: -------------------------------------------------------------------------------- 1 | var q = require('../q') 2 | , oauth = require('oauth') 3 | , bogart = require('../bogart') 4 | , _ = require('underscore') 5 | , url = require('url'); 6 | 7 | module.exports = function oauth2Middleware(config, nextApp) { 8 | if (nextApp === undefined) { 9 | return function (nextApp) { 10 | return oauth2Middleware(config, nextApp); 11 | }; 12 | } 13 | 14 | var options = { 15 | loginRoute: '/auth/login', 16 | logoutRoute: '/auth/logout', 17 | callbackRoute: '/auth/callback', 18 | modalAuthRoute: '/auth/modalauth', 19 | errorUrl: '/' 20 | }; 21 | options = _.extend(options, config); 22 | 23 | if (!options.host) { 24 | throw new Error('host is a required option') 25 | } 26 | 27 | if (!options.host.match(/^http/)) { 28 | throw new Error('host must include the protocal.'); 29 | } 30 | 31 | options.host = options.host.replace(/\/$/, ''); //remove trailing slash 32 | 33 | var OAuth2 = new oauth.OAuth2(options.clientId, options.clientSecret, '', options.authorizationURL, options.tokenURL); 34 | 35 | var authd = function(req) { 36 | if (!req.session('profile')) { 37 | return bogart.redirect(options.loginRoute + '?returnUrl=' + req.pathInfo ); 38 | } 39 | req.auth = req.auth || {}; 40 | req.auth.profile = req.session('profile'); 41 | req.auth.access_token = req.session('access_token'); 42 | return nextApp(req); 43 | }; 44 | var router = bogart.router(authd); 45 | var callbackRoute = '' 46 | router.get(options.loginRoute, function (req) { 47 | 48 | callbackRoute = options.host + options.callbackRoute; 49 | if (req.params.returnUrl) { 50 | callbackRoute += '?returnUrl=' + encodeURIComponent(req.params.returnUrl); 51 | } 52 | 53 | var url = OAuth2.getAuthorizeUrl(_.extend({ 54 | response_type: 'code', 55 | redirect_uri: callbackRoute 56 | }, config.authorizationParams)); 57 | 58 | return bogart.redirect(url); 59 | }); 60 | 61 | router.get(options.logoutRoute, function (req) { 62 | req.session('profile', undefined); 63 | 64 | return bogart.redirect('/'); 65 | }); 66 | 67 | router.get(options.callbackRoute, function (req) { 68 | var deferred = q.defer(); 69 | 70 | if (req.params.code) { 71 | var code = req.params.code; 72 | // NOTE: The module oauth (0.9.5), which is a dependency, automatically adds 73 | // a 'type=web_server' parameter to the percent-encoded data sent in 74 | // the body of the access token request. This appears to be an 75 | // artifact from an earlier draft of OAuth 2.0 (draft 22, as of the 76 | // time of this writing). This parameter is not necessary, but its 77 | // presence does not appear to cause any issues. 78 | OAuth2.getOAuthAccessToken(code, { 79 | grant_type: 'authorization_code', 80 | redirect_uri: callbackRoute 81 | }, function (err, accessToken, refreshToken) { 82 | OAuth2.getProtectedResource(options.resourceURL, accessToken, function(err, body, res) { 83 | try { 84 | if (err) { 85 | console.log(err) 86 | deferred.reject(err); 87 | } 88 | 89 | var profile = q(options.parse(body, req)).then(function (profile) { 90 | req.session('profile', profile); 91 | req.session('access_token', accessToken); 92 | 93 | return bogart.redirect(options.successUrl || req.params.returnUrl || '/'); 94 | }).fail(function (error) { 95 | var parsedUrl = url.parse(options.errorUrl, true); 96 | parsedUrl.query.message = error.message; 97 | var redirectUrl = url.format(parsedUrl); 98 | return bogart.redirect(redirectUrl); 99 | }); 100 | deferred.resolve(profile); 101 | } catch (e) { 102 | deferred.reject(e); 103 | } 104 | }); 105 | }); 106 | } 107 | 108 | return deferred.promise; 109 | }); 110 | 111 | router.post(options.modalAuthRoute, function(req) { 112 | var deffered = q.defer() 113 | , accessToken = req.params.accessToken; 114 | 115 | OAuth2.getProtectedResource(options.resourceURL, accessToken, function (err, body, res) { 116 | try { 117 | var profile = options.parse(body) 118 | if (err) { 119 | deffered.reject(err); 120 | } 121 | 122 | req.session('profile', profile); 123 | req.session('access_token', accessToken); 124 | 125 | deffered.resolve(bogart.redirect(req.params.returnUrl || '/')); 126 | } catch (e) { 127 | deffered.reject(e); 128 | } 129 | }); 130 | 131 | return deffered.promise; 132 | }); 133 | 134 | return router(nextApp); 135 | }; 136 | -------------------------------------------------------------------------------- /lib/middleware/parseForm.js: -------------------------------------------------------------------------------- 1 | var q = require('../q') 2 | , middleware = require('../middleware') 3 | , forEachable = require('../forEachable') 4 | , querystring = require('querystring'); 5 | 6 | module.exports = middleware(function (req, nextApp) { 7 | var contentType = req.headers['content-type']; 8 | 9 | if (contentType) { 10 | if (contentType.indexOf(';')) { 11 | contentType = contentType.split(';')[0]; 12 | } 13 | 14 | if (contentType === 'application/x-www-form-urlencoded' && req.body) { 15 | 16 | return q.when(forEachable.join(req.body), function (body) { 17 | req.body = querystring.parse(body); 18 | 19 | return nextApp(req); 20 | }); 21 | } 22 | } 23 | 24 | return nextApp(req); 25 | }); 26 | -------------------------------------------------------------------------------- /lib/middleware/parseJson.js: -------------------------------------------------------------------------------- 1 | var middleware = require('../middleware') 2 | , forEachable = require('../forEachable') 3 | , q = require('../q'); 4 | 5 | module.exports = middleware(function (req, nextApp) { 6 | var contentType = req.headers['content-type']; 7 | 8 | if (contentType && req.body) { 9 | if (contentType === 'application/json') { 10 | 11 | return q.when(forEachable.join(req.body), function success(body) { 12 | req.body = JSON.parse(body); 13 | 14 | return nextApp(req); 15 | }); 16 | } 17 | } 18 | 19 | return nextApp(req); 20 | }); -------------------------------------------------------------------------------- /lib/middleware/parted.js: -------------------------------------------------------------------------------- 1 | var q = require('../q') 2 | , ForEachStream = require('../forEachStream') 3 | , parted = require('parted'); 4 | 5 | module.exports = function partedMiddleware(opts, nextApp) { 6 | if (nextApp === undefined) { 7 | if (typeof opts === 'function') { 8 | nextApp = opts; 9 | opts = undefined; 10 | } else { 11 | return function (nextApp) { 12 | return partedMiddleware(opts, nextApp); 13 | }; 14 | } 15 | } 16 | 17 | var fn = parted(opts); 18 | 19 | return function (req) { 20 | var deferred = q.defer() 21 | , oldBody = req.body 22 | , emitter; 23 | 24 | if (oldBody === undefined) { 25 | return nextApp(req); 26 | } 27 | 28 | emitter = new ForEachStream(oldBody); 29 | req.on = emitter.on.bind(emitter); 30 | req.pipe = emitter.pipe.bind(emitter); 31 | 32 | delete req.body; 33 | 34 | fn(req, null, function () { 35 | q.when(nextApp(req), function (resp) { 36 | deferred.resolve(resp); 37 | }, deferred.reject); 38 | }); 39 | 40 | return deferred.promise; 41 | }; 42 | }; -------------------------------------------------------------------------------- /lib/middleware/renderView.js: -------------------------------------------------------------------------------- 1 | var middleware = require('./middleware'); 2 | 3 | /* 4 | * Renders `template` using `viewEngine`. 5 | * ViewEngine#respond is called with the results of 6 | * the `next` apps response. 7 | * 8 | * Example: 9 | * 10 | * var router = bogart.router(); 11 | * router.get('/', bogart.middleware.renderView('index.html'), function (req) { 12 | * return { locals: { title: 'Bogart' }, layout: true }; 13 | * }); 14 | * 15 | * @param {ViewEngine} viewEngine 16 | * @param {String} template The template to render. 17 | */ 18 | function RenderView(viewEngine, template) { 19 | return middleware(req, next) { 20 | return next(req).then(function (renderOpts) { 21 | return viewEngine.respond(template, renderOpts); 22 | }); 23 | }; 24 | } 25 | 26 | module.exports = RenderView; 27 | -------------------------------------------------------------------------------- /lib/middleware/session/cookieDataProvider.js: -------------------------------------------------------------------------------- 1 | var uuid = require("node-uuid"), 2 | security = require("../../security"), 3 | util = require("../../util"), 4 | SESSION_DATA_COOKIE = "bogart_session_data", 5 | DEFAULT_SESSION_LIFETIME = 1800 /* 30 mins */; 6 | 7 | function CookieDataProvider(config) { 8 | config = config || {}; 9 | config.lifetime = config.lifetime || DEFAULT_SESSION_LIFETIME; 10 | 11 | if (!config.secret) { 12 | throw new Error('CookieDataProvider `secret` is required: new CookieDataProvider({ secret: "my-super-secret" })'); 13 | } 14 | 15 | this.encrypt = config.encrypt || security.encrypt; 16 | this.decrypt = config.decrypt || security.decrypt; 17 | 18 | this.config = config; 19 | this.cookieKey = encodeURIComponent(this.encrypt(SESSION_DATA_COOKIE, this.config.secret)); 20 | } 21 | 22 | /** 23 | * Called once at the beginning of the request 24 | */ 25 | CookieDataProvider.prototype.loadSession = function(req, sessionId) { 26 | var cookie = util.parseCookies(req)[this.cookieKey]; 27 | 28 | var decryptedCookie, session; 29 | try { 30 | decryptedCookie = this.decrypt(decodeURIComponent(cookie), this.config.secret); 31 | session = cookie ? JSON.parse(decryptedCookie) : {}; 32 | } catch (err) { 33 | session = {}; 34 | } 35 | 36 | return session; 37 | }; 38 | 39 | /** 40 | * Called once at the end of the request 41 | */ 42 | CookieDataProvider.prototype.save = function(req, res, sessionId) { 43 | return this.setCookie(req, res, sessionId, this.config.lifetime); 44 | }; 45 | 46 | CookieDataProvider.prototype.destroy = function (req, res, sessionId) { 47 | return this.setCookie(req, res, sessionId, -1); 48 | }; 49 | 50 | CookieDataProvider.prototype.setCookie = function (req, res, sessionId, expiresInSeconds) { 51 | var cookieKey = this.cookieKey; 52 | var secret = this.config.secret; 53 | 54 | res = res || {}; 55 | res.headers = res.headers || {}; 56 | res.headers["Set-Cookie"] = res.headers["Set-Cookie"] || []; 57 | 58 | var expires = new Date(); 59 | expires.setSeconds(expires.getSeconds() + expiresInSeconds); 60 | 61 | var sData = expiresInSeconds > 0 ? JSON.stringify(req.env.session) : '{}'; 62 | 63 | var cookie = cookieKey + "=" + encodeURIComponent(this.encrypt(sData, secret)) + "; Path=/; Expires=" + expires.toUTCString() + ";"; 64 | 65 | res = util.ensureSetCookieArray(res); 66 | res.headers["Set-Cookie"] = res.headers["Set-Cookie"].filter(function(el) { 67 | return !(new RegExp("^" + cookieKey + "=").test(el)); 68 | }); 69 | res.headers["Set-Cookie"].push(cookie); 70 | 71 | return res; 72 | } 73 | 74 | module.exports = CookieDataProvider; 75 | -------------------------------------------------------------------------------- /lib/middleware/session/idProvider.js: -------------------------------------------------------------------------------- 1 | var uuid = require("node-uuid"), 2 | security = require("../../security"), 3 | util = require("../../util"), 4 | format = require('util').format, 5 | SESSION_ID_COOKIE = "bogart_session_id", 6 | DEFAULT_SESSION_LIFETIME = 1800; 7 | 8 | exports.IdProvider = IdProvider; 9 | 10 | function IdProvider(config) { 11 | config = config || {}; 12 | config.lifetime = config.lifetime || DEFAULT_SESSION_LIFETIME; 13 | 14 | this.config = config; 15 | this.cookieKey = encodeURIComponent(security.encrypt(SESSION_ID_COOKIE, this.config.secret)); 16 | } 17 | 18 | /** 19 | * Called once at the beginning of each request. 20 | */ 21 | IdProvider.prototype.getSessionId = function(req) { 22 | var cookie = util.parseCookies(req)[this.cookieKey]; 23 | var id; 24 | if (!cookie) { 25 | id = uuid(); 26 | } else { 27 | id = security.decrypt(decodeURIComponent(cookie), this.config.secret); 28 | } 29 | return id; 30 | }; 31 | 32 | 33 | /** 34 | * Modifies the response to include the SessionId 35 | * 36 | * @param {Object} req JSGI Request 37 | * @param {Object} res JSGI Response 38 | * @param {String} sessionId Unique identifier of the Session. 39 | * 40 | * @returns {Object} JSGI Response 41 | */ 42 | IdProvider.prototype.save = function(req, res, sessionId) { 43 | return setCookie(req, res, sessionId, this.config.lifetime, this.config.secret, this.cookieKey); 44 | }; 45 | 46 | IdProvider.prototype.destroy = function (req, res, sessionId) { 47 | return setCookie(req, res, '', -1, this.config.secret, this.cookieKey); 48 | }; 49 | 50 | function setCookie(req, res, sessionId, expiresInSeconds, secret, cookieKey) { 51 | res = res || {}; 52 | res.headers = res.headers || {}; 53 | res.headers["Set-Cookie"] = res.headers["Set-Cookie"] || []; 54 | var expires = new Date(); 55 | expires.setSeconds(expires.getSeconds() + expiresInSeconds); 56 | 57 | var cookie = sessionCookie(sessionId, secret, expires, cookieKey); 58 | 59 | res = util.ensureSetCookieArray(res); 60 | res.headers["Set-Cookie"] = res.headers["Set-Cookie"].filter(isNotSessionIdCookie(cookieKey)); 61 | res.headers["Set-Cookie"].push(cookie); 62 | 63 | return res; 64 | } 65 | 66 | function isNotSessionIdCookie(cookieKey) { 67 | return function (el) { 68 | return !(new RegExp("^" + cookieKey + "=").test(el)); 69 | } 70 | } 71 | 72 | function sessionCookie(sessionId, secret, expires, cookieKey) { 73 | return format("%s=%s; Path=/; Expires=%s;", 74 | cookieKey, 75 | encodeSessionId(sessionId, secret), 76 | expires.toUTCString()); 77 | } 78 | 79 | function encodeSessionId(sessionId, secret) { 80 | return encodeURIComponent(security.encrypt(sessionId, secret)); 81 | } 82 | -------------------------------------------------------------------------------- /lib/middleware/session/session.js: -------------------------------------------------------------------------------- 1 | var Q = require('../../q'), 2 | when = Q.when, 3 | DefaultIdProvider = require('./idProvider').IdProvider, 4 | CookieDataProvider = require('./cookieDataProvider'); 5 | 6 | exports.Session = function (config, nextApp) { 7 | if (nextApp === undefined) { 8 | 9 | if (typeof config === 'function') { 10 | nextApp = config; 11 | config = {}; 12 | } else { 13 | return function (nextApp) { 14 | return exports.Session(config, nextApp); 15 | }; 16 | } 17 | } 18 | 19 | config = config || {}; 20 | config.options = config.options || {}; 21 | config.options.idProvider = config.options.idProvider || {}; 22 | config.options.store = config.options.store || {}; 23 | config.options.idProvider.lifetime = config.lifetime || undefined; 24 | config.options.store.lifetime = config.lifetime || undefined; 25 | 26 | if (config.encryptionKey) { 27 | warnConfigChange('encryptionKey', 'secret'); 28 | config.secret = config.encryptionKey; 29 | } 30 | 31 | if (config.dataProvider) { 32 | warnConfigChange('dataProvider', 'store'); 33 | config.store = config.dataProvider; 34 | } 35 | 36 | if (config.options.dataProvider) { 37 | warnConfigChange('options.dataProvider', 'options.store'); 38 | config.options.store = config.options.dataProvider; 39 | } 40 | 41 | if (config.secret) { 42 | config.options.idProvider.secret = config.secret; 43 | config.options.store.secret = config.secret; 44 | } 45 | 46 | var idProvider = config.idProvider || new DefaultIdProvider(config.options.idProvider); 47 | var dataProvider = config.store || new CookieDataProvider(config.options.store); 48 | 49 | return function (req) { 50 | var sessionId = idProvider.getSessionId(req); 51 | 52 | return when(dataProvider.loadSession(req, sessionId), function (session) { 53 | req.env = req.env || {}; 54 | req.env.session = session; 55 | 56 | // set up the session function 57 | req.session = function () { 58 | var args = Array.prototype.slice.call(arguments); 59 | if (args.length === 2) { 60 | session[args[0]] = args[1]; 61 | } else { 62 | return session[args[0]]; 63 | } 64 | }; 65 | 66 | req.session.keys = function () { 67 | return Object.keys(session); 68 | }; 69 | 70 | req.session.hasKey = function (key) { 71 | return this.keys().filter(function (k) { return k === key; }).length > 0; 72 | }; 73 | 74 | req.session.remove = function (key) { 75 | delete session[key]; 76 | }; 77 | 78 | req.session.destroy = function () { 79 | req.session._destroy = true; 80 | }; 81 | 82 | return when(nextApp(req), function (res) { 83 | if (req.session._destroy === true) { 84 | res = idProvider.destroy(req, res, sessionId) || res; 85 | return when(dataProvider.destroy(req, res, sessionId), function (dpResp) { 86 | return dpResp || res; 87 | }); 88 | } else { 89 | res = idProvider.save(req, res, sessionId) || res; 90 | 91 | return when(dataProvider.save(req, res, sessionId), function (dpResp) { 92 | return dpResp || res; 93 | }); 94 | } 95 | }); 96 | }); 97 | }; 98 | }; 99 | 100 | function warnConfigChange(oldProperty, newProperty) { 101 | console.warn('The bogart.session configuration `'+oldProperty+'` has been changed to `'+newProperty+'`. \ 102 | Please update your configuration.') 103 | } 104 | 105 | exports.Session.DefaultIdProvider = DefaultIdProvider; 106 | exports.Session.DefaultDataProvider = CookieDataProvider; -------------------------------------------------------------------------------- /lib/middleware/stringReturnAdapter.js: -------------------------------------------------------------------------------- 1 | var q = require('../q') 2 | , merge = require('../util').merge; 3 | 4 | module.exports = function stringReturnAdapter(responseDefaults, nextApp) { 5 | if (nextApp === undefined) { 6 | if (typeof responseDefaults === 'function') { 7 | nextApp = responseDefaults; 8 | responseDefaults = { 9 | status: 200, 10 | headers: { 'Content-Type': 'text/html' } 11 | }; 12 | } else { 13 | return function (nextApp) { 14 | return stringReturnAdapter(responseDefaults, nextApp); 15 | }; 16 | } 17 | } 18 | 19 | return function (req) { 20 | return q.when(nextApp(req), function (resp) { 21 | if (typeof resp === 'string') { 22 | return merge({}, responseDefaults, { body: [ resp ] }); 23 | } 24 | 25 | return resp; 26 | }); 27 | }; 28 | }; -------------------------------------------------------------------------------- /lib/middleware/twitter.js: -------------------------------------------------------------------------------- 1 | var middleware = require('../middleware') 2 | , _ = require('underscore'); 3 | 4 | function parseTwitterProfile(body) { 5 | if (body) { 6 | var o = JSON.parse(body), 7 | profile = { 8 | provider: 'twitter', 9 | id: o.id, 10 | userName: o.screen_name, 11 | email: o.screen_name, 12 | imageLocation: o.profile_image_url 13 | }; 14 | if (o.name) { 15 | var nameSplit = o.name.split(' '); 16 | profile.givenName = nameSplit[0]; 17 | profile.surName = nameSplit[1]; 18 | } 19 | if (o.location) { 20 | var locationSplit = o.location.split(','); 21 | profile.city = locationSplit[0]; 22 | profile.state = locationSplit[1].trim(); 23 | } 24 | return profile; 25 | } 26 | } 27 | 28 | module.exports = twitterMiddleware; 29 | 30 | function twitterMiddleware(config, nextApp) { 31 | if (nextApp === undefined) { 32 | return function (nextApp) { 33 | return twitterMiddleware(config, nextApp); 34 | }; 35 | } 36 | 37 | var twitter_strategy = { 38 | authorizationURL: 'https://api.twitter.com/oauth/authorize', 39 | requestTokenURL: 'https://api.twitter.com/oauth/request_token', 40 | accessTokenURL: 'https://api.twitter.com/oauth/access_token', 41 | resourceURL: 'https://api.twitter.com/1.1/users/show.json', 42 | parse: twitterMiddleware.parseTwitterProfile, 43 | resourceURLParams: [ 'user_id' ] 44 | }; 45 | 46 | var oauthOptions = _.extend(twitter_strategy, config); 47 | 48 | return middleware.oauth(oauthOptions, nextApp); 49 | }; 50 | 51 | twitterMiddleware.parseTwitterProfile = parseTwitterProfile; 52 | -------------------------------------------------------------------------------- /lib/middleware/validateResponse.js: -------------------------------------------------------------------------------- 1 | var q = require('../q') 2 | , middleware = require('../middleware'); 3 | 4 | /** 5 | * Validates that the response from nextApp is a valid JSGI response. 6 | * Rejects the promise if the response is invalid. 7 | * 8 | * @param {Function} nextApp The next application in the JSGI chain. 9 | * @returns {Function} JSGI response. 10 | */ 11 | module.exports = middleware(function (req, nextApp) { 12 | return q.whenCall(function() { return nextApp(req); }, function(resp) { 13 | if (!resp) { 14 | return q.reject('Response must be an object.'); 15 | } 16 | 17 | if (!resp.body) { 18 | return q.reject('Response must have a body property.'); 19 | } 20 | 21 | if (!resp.body.forEach) { 22 | return q.reject('Response body must have a forEach method.'); 23 | } 24 | 25 | if (typeof resp.body.forEach !== 'function') { 26 | return q.reject('Response body has a forEach method but the forEach method is not a function.'); 27 | } 28 | 29 | if (!resp.status) { 30 | return q.reject('Response must have a status property.'); 31 | } 32 | 33 | if (typeof resp.status.constructor !== '[Function: Number]') { 34 | return q.reject('Response has a status property but the status property must be a number.'); 35 | } 36 | 37 | return resp; 38 | }, function(err) { 39 | // Workaround for fact that whenCall calls rejectCallback even if one is not provided. 40 | throw err; 41 | }); 42 | }); -------------------------------------------------------------------------------- /lib/mimetypes.js: -------------------------------------------------------------------------------- 1 | 2 | // -- tlrobinson 3 | 4 | /** 5 | * @module 6 | */ 7 | 8 | /*whatsupdoc*/ 9 | 10 | // returns MIME type for extension, or fallback, or octet-steam 11 | /** 12 | * @param {String} extension a file extension, including the prefix 13 | * `"."`. 14 | * @param {String} fallback a MIME type to use if no MIME type 15 | * corresponds to the prefix. Defaults to 16 | * `"application/octet-stream"`. 17 | * @returns {String} a MIME type 18 | */ 19 | exports.mimeType = function (extension, fallback) { 20 | return exports.mimeTypes[extension.toLowerCase()] || 21 | fallback || 22 | "application/octet-stream"; 23 | }; 24 | 25 | // List of most common mime-types, stolen from Rack. 26 | /** 27 | * {Object * String} an object mapping file extensions to MIME types. 28 | */ 29 | exports.mimeTypes = { 30 | ".3gp" : "video/3gpp", 31 | ".a" : "application/octet-stream", 32 | ".ai" : "application/postscript", 33 | ".aif" : "audio/x-aiff", 34 | ".aiff" : "audio/x-aiff", 35 | ".asc" : "application/pgp-signature", 36 | ".asf" : "video/x-ms-asf", 37 | ".asm" : "text/x-asm", 38 | ".asx" : "video/x-ms-asf", 39 | ".atom" : "application/atom+xml", 40 | ".au" : "audio/basic", 41 | ".avi" : "video/x-msvideo", 42 | ".bat" : "application/x-msdownload", 43 | ".bin" : "application/octet-stream", 44 | ".bmp" : "image/bmp", 45 | ".bz2" : "application/x-bzip2", 46 | ".c" : "text/x-c", 47 | ".cab" : "application/vnd.ms-cab-compressed", 48 | ".cc" : "text/x-c", 49 | ".chm" : "application/vnd.ms-htmlhelp", 50 | ".class" : "application/octet-stream", 51 | ".com" : "application/x-msdownload", 52 | ".conf" : "text/plain", 53 | ".cpp" : "text/x-c", 54 | ".crt" : "application/x-x509-ca-cert", 55 | ".css" : "text/css", 56 | ".csv" : "text/csv", 57 | ".cxx" : "text/x-c", 58 | ".deb" : "application/x-debian-package", 59 | ".der" : "application/x-x509-ca-cert", 60 | ".diff" : "text/x-diff", 61 | ".djv" : "image/vnd.djvu", 62 | ".djvu" : "image/vnd.djvu", 63 | ".dll" : "application/x-msdownload", 64 | ".dmg" : "application/octet-stream", 65 | ".doc" : "application/msword", 66 | ".dot" : "application/msword", 67 | ".dtd" : "application/xml-dtd", 68 | ".dvi" : "application/x-dvi", 69 | ".ear" : "application/java-archive", 70 | ".eml" : "message/rfc822", 71 | ".eps" : "application/postscript", 72 | ".exe" : "application/x-msdownload", 73 | ".f" : "text/x-fortran", 74 | ".f77" : "text/x-fortran", 75 | ".f90" : "text/x-fortran", 76 | ".flv" : "video/x-flv", 77 | ".for" : "text/x-fortran", 78 | ".gem" : "application/octet-stream", 79 | ".gemspec" : "text/x-script.ruby", 80 | ".gif" : "image/gif", 81 | ".gz" : "application/x-gzip", 82 | ".h" : "text/x-c", 83 | ".hh" : "text/x-c", 84 | ".htm" : "text/html", 85 | ".html" : "text/html", 86 | ".ico" : "image/vnd.microsoft.icon", 87 | ".ics" : "text/calendar", 88 | ".ifb" : "text/calendar", 89 | ".iso" : "application/octet-stream", 90 | ".jar" : "application/java-archive", 91 | ".java" : "text/x-java-source", 92 | ".jnlp" : "application/x-java-jnlp-file", 93 | ".jpeg" : "image/jpeg", 94 | ".jpg" : "image/jpeg", 95 | ".js" : "application/javascript", 96 | ".json" : "application/json", 97 | ".log" : "text/plain", 98 | ".m3u" : "audio/x-mpegurl", 99 | ".m4v" : "video/mp4", 100 | ".man" : "text/troff", 101 | ".manifest": "text/cache-manifest", 102 | ".mathml" : "application/mathml+xml", 103 | ".mbox" : "application/mbox", 104 | ".mdoc" : "text/troff", 105 | ".me" : "text/troff", 106 | ".mid" : "audio/midi", 107 | ".midi" : "audio/midi", 108 | ".mime" : "message/rfc822", 109 | ".mml" : "application/mathml+xml", 110 | ".mng" : "video/x-mng", 111 | ".mov" : "video/quicktime", 112 | ".mp3" : "audio/mpeg", 113 | ".mp4" : "video/mp4", 114 | ".mp4v" : "video/mp4", 115 | ".mpeg" : "video/mpeg", 116 | ".mpg" : "video/mpeg", 117 | ".ms" : "text/troff", 118 | ".msi" : "application/x-msdownload", 119 | ".odp" : "application/vnd.oasis.opendocument.presentation", 120 | ".ods" : "application/vnd.oasis.opendocument.spreadsheet", 121 | ".odt" : "application/vnd.oasis.opendocument.text", 122 | ".ogg" : "application/ogg", 123 | ".p" : "text/x-pascal", 124 | ".pas" : "text/x-pascal", 125 | ".pbm" : "image/x-portable-bitmap", 126 | ".pdf" : "application/pdf", 127 | ".pem" : "application/x-x509-ca-cert", 128 | ".pgm" : "image/x-portable-graymap", 129 | ".pgp" : "application/pgp-encrypted", 130 | ".pkg" : "application/octet-stream", 131 | ".pl" : "text/x-script.perl", 132 | ".pm" : "text/x-script.perl-module", 133 | ".png" : "image/png", 134 | ".pnm" : "image/x-portable-anymap", 135 | ".ppm" : "image/x-portable-pixmap", 136 | ".pps" : "application/vnd.ms-powerpoint", 137 | ".ppt" : "application/vnd.ms-powerpoint", 138 | ".ps" : "application/postscript", 139 | ".psd" : "image/vnd.adobe.photoshop", 140 | ".py" : "text/x-script.python", 141 | ".qt" : "video/quicktime", 142 | ".ra" : "audio/x-pn-realaudio", 143 | ".rake" : "text/x-script.ruby", 144 | ".ram" : "audio/x-pn-realaudio", 145 | ".rar" : "application/x-rar-compressed", 146 | ".rb" : "text/x-script.ruby", 147 | ".rdf" : "application/rdf+xml", 148 | ".roff" : "text/troff", 149 | ".rpm" : "application/x-redhat-package-manager", 150 | ".rss" : "application/rss+xml", 151 | ".rtf" : "application/rtf", 152 | ".ru" : "text/x-script.ruby", 153 | ".s" : "text/x-asm", 154 | ".sgm" : "text/sgml", 155 | ".sgml" : "text/sgml", 156 | ".sh" : "application/x-sh", 157 | ".sig" : "application/pgp-signature", 158 | ".snd" : "audio/basic", 159 | ".so" : "application/octet-stream", 160 | ".svg" : "image/svg+xml", 161 | ".svgz" : "image/svg+xml", 162 | ".swf" : "application/x-shockwave-flash", 163 | ".t" : "text/troff", 164 | ".tar" : "application/x-tar", 165 | ".tbz" : "application/x-bzip-compressed-tar", 166 | ".tcl" : "application/x-tcl", 167 | ".tex" : "application/x-tex", 168 | ".texi" : "application/x-texinfo", 169 | ".texinfo" : "application/x-texinfo", 170 | ".text" : "text/plain", 171 | ".tif" : "image/tiff", 172 | ".tiff" : "image/tiff", 173 | ".torrent" : "application/x-bittorrent", 174 | ".tr" : "text/troff", 175 | ".txt" : "text/plain", 176 | ".vcf" : "text/x-vcard", 177 | ".vcs" : "text/x-vcalendar", 178 | ".vrml" : "model/vrml", 179 | ".war" : "application/java-archive", 180 | ".wav" : "audio/x-wav", 181 | ".wma" : "audio/x-ms-wma", 182 | ".wmv" : "video/x-ms-wmv", 183 | ".wmx" : "video/x-ms-wmx", 184 | ".wrl" : "model/vrml", 185 | ".wsdl" : "application/wsdl+xml", 186 | ".xbm" : "image/x-xbitmap", 187 | ".xhtml" : "application/xhtml+xml", 188 | ".xls" : "application/vnd.ms-excel", 189 | ".xml" : "application/xml", 190 | ".xpm" : "image/x-xpixmap", 191 | ".xsl" : "application/xml", 192 | ".xslt" : "application/xslt+xml", 193 | ".yaml" : "text/yaml", 194 | ".yml" : "text/yaml", 195 | ".zip" : "application/zip" 196 | }; 197 | 198 | -------------------------------------------------------------------------------- /lib/q.js: -------------------------------------------------------------------------------- 1 | var q = require('q') 2 | , slice = Array.prototype.slice; 3 | 4 | /** 5 | * Wraps a Node.JS style asynchronous function `function(err, result) {}` 6 | * to return a `Promise`. 7 | * 8 | * @param {Function} nodeAsyncFn A node style async function expecting a callback as its last parameter. 9 | * @param {Object} context Optional, if provided nodeAsyncFn is run with `this` being `context`. 10 | * 11 | * @returns {Function} A function that returns a promise. 12 | */ 13 | q.promisify = function(nodeAsyncFn, context) { 14 | return function() { 15 | var defer = q.defer() 16 | , args = slice.call(arguments); 17 | 18 | args.push(function(err, val) { 19 | if (err !== null) { 20 | return defer.reject(err); 21 | } 22 | 23 | return defer.resolve(val); 24 | }); 25 | 26 | nodeAsyncFn.apply(context || {}, args); 27 | 28 | return defer.promise; 29 | }; 30 | }; 31 | 32 | if (!q.execute) { 33 | /** 34 | * Runs a Node.JS style asynchronous function `function(err, result) {}`, but returns a Promise instead. 35 | * @param {Function} Node.JS compatible async function which takes a callback as its last argument 36 | * @returns {Promise} A promise for the return value from the callback from the function 37 | */ 38 | q.execute = function(asyncFunction){ 39 | var defer = q.defer() 40 | , args = slice.call(arguments, 1); 41 | 42 | args.push(function(err, result){ 43 | if(err !== null) { 44 | defer.reject(err); 45 | } 46 | else { 47 | if(arguments.length > 2){ 48 | // Return an array if multiple success values. 49 | Array.prototype.shift.call(arguments, 1); 50 | defer.resolve(arguments); 51 | } 52 | else{ 53 | defer.resolve(result); 54 | } 55 | } 56 | }); 57 | asyncFunction.apply(this, args); 58 | return defer.promise; 59 | }; 60 | } 61 | 62 | if (!q.resolve) { 63 | q.resolve = function(val) { 64 | var deferred = q.defer(); 65 | 66 | deferred.resolve(val); 67 | 68 | return deferred.promise; 69 | }; 70 | } 71 | 72 | if (!q.reject) { 73 | q.reject = function(reason) { 74 | var deferred = q.defer(); 75 | 76 | deferred.reject(reason); 77 | 78 | return deferred.promise; 79 | } 80 | } 81 | 82 | if (!q.whenCall) { 83 | q.whenCall = function(callback, resolvedCallback, errorCallback, progressCallback) { 84 | try { 85 | return q.when(callback(), resolvedCallback, errorCallback, progressCallback); 86 | } catch (err) { 87 | return errorCallback(err); 88 | } 89 | } 90 | } 91 | 92 | module.exports = q; 93 | -------------------------------------------------------------------------------- /lib/request.js: -------------------------------------------------------------------------------- 1 | 2 | var util = require('./util'); 3 | 4 | module.exports = function(jsgiReq) { 5 | return Object.create(jsgiReq, { 6 | search: getter(function() { 7 | return util.extractSearch(jsgiReq); 8 | }), 9 | isXMLHttpRequest: getter(function() { 10 | return !util.no(this.headers['x-requested-with']); 11 | }), 12 | routeParams: { 13 | value: {}, 14 | enumerable: true 15 | }, 16 | params: getter(function() { 17 | return util.merge({}, this.routeParams, this.search, this.body); 18 | }) 19 | }); 20 | }; 21 | 22 | /** 23 | * Helper function to create property descriptor that is enumerable 24 | * and has a getter. 25 | * 26 | * @param fn {Function} The getter function. 27 | * @api private 28 | */ 29 | function getter(fn) { 30 | return { 31 | get: fn, 32 | enumerable: true 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /lib/router.js: -------------------------------------------------------------------------------- 1 | var 2 | util = require('./util'), 3 | EventEmitter = require('events').EventEmitter, 4 | Q = require('./q'), 5 | when = Q.when, 6 | inherits = require('util').inherits, 7 | slice = Array.prototype.slice, 8 | _ = require('underscore'); 9 | 10 | var 11 | httpMethod = { 12 | GET: "get", 13 | POST: "post", 14 | PUT: "put", 15 | DELETE: "delete" 16 | }, 17 | restMethod = { 18 | SHOW: httpMethod.GET, 19 | CREATE: httpMethod.POST, 20 | UPDATE: httpMethod.PUT, 21 | DESTROY: httpMethod.DELETE 22 | }, 23 | PATH_PARAMETER_REPLACEMENT = "([^\/\?]+)", 24 | PATH_PARAMETERS = /:([\w\d]+)/g; 25 | 26 | exports.bogartEvent = { 27 | BEFORE_ADD_ROUTE: "beforeAddRoute", 28 | AFTER_ADD_ROUTE: "afterAddRoute" 29 | }; 30 | 31 | function defaultNotFoundApp(req) { 32 | var body = 'Not Found'; 33 | if (req.pathInfo) { 34 | body += ': ' + req.pathInfo; 35 | } 36 | 37 | return { 38 | status: 404, 39 | body: [body], 40 | headers: { 'Content-Length': Buffer.byteLength(body, 'utf-8'), 'Content-Type': 'text/html' } 41 | }; 42 | } 43 | 44 | exports.Router = Router; 45 | function Router() { 46 | if (!this.respond) { 47 | return new Router(); 48 | } 49 | 50 | var settings = {} 51 | 52 | var app = function (notFoundApp) { 53 | if (notFoundApp) { 54 | if (typeof notFoundApp !== 'function') { 55 | throw new Error('Invalid argument: router expects a function.') 56 | } 57 | 58 | app.nextApp = notFoundApp; 59 | } else { 60 | app.nextApp = app.nextApp || defaultNotFoundApp; 61 | } 62 | 63 | return function (req) { 64 | return when(req, function(req) { 65 | try { 66 | return app.respond(require('./request')(req)); 67 | } catch (err) { 68 | return Q.reject(err); 69 | } 70 | }); 71 | }; 72 | }; 73 | 74 | app.routes = {}; 75 | app.beforeCallbacks = []; 76 | app.afterCallbacks = []; 77 | 78 | app.setting = function(name, val) { 79 | if (val === undefined) { 80 | return settings[name]; 81 | } 82 | 83 | settings[name] = val; 84 | return this; 85 | }; 86 | 87 | EventEmitter.call(app); 88 | 89 | _.extend(app, this.__proto__); 90 | _.extend(app, this.__proto__.__proto__); 91 | 92 | return app; 93 | } 94 | 95 | inherits(Router, EventEmitter); 96 | 97 | /** 98 | * Register a callback to happen before bogart handlers are invoked to 99 | * handle a request. Multiple 'before' callbacks may be registered. 100 | * 101 | * @param {Function} cb Callback to happen before route handler is invoked. 102 | */ 103 | Router.prototype.before = function(cb) { 104 | this.beforeCallbacks.push(cb); 105 | }; 106 | 107 | /** 108 | * Register a callback to happen after bogart handlers are invoked to 109 | * handle a request. Multiple 'after' callbacks may be registered. 110 | * 111 | * @param {Function} cb Callback to happen after route handler is invoked. 112 | */ 113 | Router.prototype.after = function(cb) { 114 | this.afterCallbacks.push(cb); 115 | }; 116 | 117 | /** 118 | * Register a route 119 | * @param {String} method Http Verb e.g. 'GET', 'POST', 'PUT', 'DELETE' 120 | * @param {String} path Path for the route 121 | * @param {Function} handler Function to execute when the route is accessed 122 | */ 123 | Router.prototype.route = function(method, path /*, handlers... */) { 124 | var paramNames, route, originalPath = path 125 | , args = slice.call(arguments); 126 | 127 | method = args.shift(); 128 | path = args.shift(); 129 | apps = args; 130 | 131 | if (path.constructor === String) { 132 | paramNames = path.match(PATH_PARAMETERS) || []; 133 | paramNames = paramNames.map(function(x) { return x.substring(1); }); 134 | 135 | path = new RegExp("^"+path.replace(/\./, '\\.').replace(/\*/g, '(.+)').replace(PATH_PARAMETERS, PATH_PARAMETER_REPLACEMENT)+'$'); 136 | } 137 | 138 | route = makeRoute({ path: path, paramNames: paramNames, apps: apps, originalPath: originalPath }); 139 | 140 | this.emit(exports.bogartEvent.BEFORE_ADD_ROUTE, this, route); 141 | 142 | this.routes[method] = this.routes[method] || []; 143 | this.routes[method].push(route); 144 | 145 | this.emit(exports.bogartEvent.AFTER_ADD_ROUTE, this, route); 146 | 147 | return this; 148 | }; 149 | 150 | Router.prototype.handler = function(verb, path) { 151 | verb = verb.toLowerCase(); 152 | var route; 153 | 154 | if (this.routes[verb]) { 155 | for (var i=0;i indx) { 298 | req.routeParams[route.paramNames[indx]] = val; 299 | } else if (val !== undefined) { 300 | req.routeParams.splat = req.routeParams.splat || []; 301 | req.routeParams.splat.push(val); 302 | } 303 | }); 304 | } 305 | } 306 | } 307 | }); 308 | } -------------------------------------------------------------------------------- /lib/security.js: -------------------------------------------------------------------------------- 1 | var crypto = require('crypto'), 2 | secretCipherPass = "SDFOIJSDFJLKJ"; 3 | 4 | exports.hash = function(value) { 5 | var shasum = crypto.createHash('sha1'); 6 | shasum.update(value); 7 | return shasum.digest('hex'); 8 | } 9 | 10 | exports.encrypt = function(valueToEncrypt) { 11 | var key = arguments[1] || secretCipherPass; 12 | var cipher = crypto.createCipher('aes-256-cbc', key); 13 | 14 | var str = cipher.update(valueToEncrypt, 'utf8', 'hex'); 15 | str += cipher.final('hex') || ''; 16 | return str; 17 | } 18 | 19 | exports.decrypt = function(valueToDecrypt) { 20 | var key = arguments[1] || secretCipherPass; 21 | var cipher = crypto.createDecipher('aes-256-cbc', key); 22 | 23 | 24 | var str = cipher.update(valueToDecrypt, 'hex', 'utf8'); 25 | str += cipher.final('utf8') || ''; 26 | return str; 27 | } 28 | -------------------------------------------------------------------------------- /lib/stream.js: -------------------------------------------------------------------------------- 1 | var Q = require('./q') 2 | , Stream = require('stream').Stream 3 | , util = require('util') 4 | , ForEachStream = require('./forEachStream'); 5 | 6 | 7 | function pumpForEachStream(src, dest) { 8 | var deferred = Q.defer() 9 | , forEachStream = new ForEachStream(src) 10 | , isRejected = false; 11 | 12 | forEachStream.pipe(dest); 13 | 14 | dest.on('finish', function() { 15 | if (!isRejected) { 16 | deferred.resolve(); 17 | } 18 | }); 19 | 20 | forEachStream.on('error', function(err) { 21 | deferred.reject(err); 22 | }); 23 | 24 | return deferred.promise; 25 | } 26 | 27 | function pumpStream(src, dest) { 28 | var deferred = Q.defer() 29 | , isRejected = false; 30 | 31 | src.pipe(dest); 32 | 33 | dest.on('finish', function() { 34 | if (!isRejected) { 35 | deferred.resolve(); 36 | } 37 | }); 38 | 39 | src.on('error', function(err) { 40 | deferred.reject(err); 41 | }); 42 | 43 | return deferred.promise; 44 | } 45 | 46 | /** 47 | * Pipes data from source to dest. 48 | * 49 | * @param {ForEachable | ReadableStream} src Source of data 50 | * @param {WriteableStream} dest Write data from src to dest 51 | * 52 | * @returns {Promise} A promise that will be resolved when the pumping is completed 53 | */ 54 | exports.pump = function(src, dest) { 55 | if (src.forEach) { 56 | return pumpForEachStream(src, dest); 57 | } else { 58 | return pumpStream(src, dest); 59 | } 60 | }; 61 | 62 | exports.ForEachStream = require('./forEachStream'); -------------------------------------------------------------------------------- /lib/util.js: -------------------------------------------------------------------------------- 1 | var _ = require('underscore'); 2 | 3 | exports.no = function(value) { 4 | return value === undefined || value === null; 5 | }; 6 | 7 | exports.extractBody = function(jsgiReq) { 8 | return when(jsgiReq.input, function(input) { 9 | return require('querystring').parse(input); 10 | }); 11 | }; 12 | 13 | exports.extractSearch = function(jsgiReq) { 14 | return require('querystring').parse(jsgiReq.queryString); 15 | }; 16 | 17 | exports.merge = _.extend; 18 | 19 | /** 20 | * Taken under terms of the MIT license from: https://github.com/senchalabs/connect 21 | * Parse the given cookie string into an object. 22 | * 23 | * @param {String} str 24 | * @return {Object} 25 | * @api public 26 | */ 27 | exports.parseCookies = function(arg){ 28 | var str = arg.headers && arg.headers.cookie ? arg.headers.cookie : arg; 29 | if(Object.prototype.toString.call(str) !== "[object String]") { 30 | return {}; 31 | } 32 | var obj = {} 33 | , pairs = str.split(/[;,] */); 34 | for (var i = 0, len = pairs.length; i < len; ++i) { 35 | var pair = pairs[i] 36 | , eqlIndex = pair.indexOf('=') 37 | , key = pair.substr(0, eqlIndex).trim() 38 | , val = pair.substr(++eqlIndex, pair.length).trim(); 39 | 40 | // quoted values 41 | if ('"' == val[0]) val = val.slice(1, -1); 42 | 43 | // only assign once 44 | if (undefined == obj[key]) { 45 | val = val.replace(/\+/g, ' '); 46 | try { 47 | obj[key] = decodeURIComponent(val); 48 | } catch (err) { 49 | if (err instanceof URIError) { 50 | obj[key] = val; 51 | } else { 52 | throw err; 53 | } 54 | } 55 | } 56 | } 57 | return obj; 58 | }; 59 | 60 | /** 61 | * Ensures that the Set-Cookie header in the response is an array 62 | */ 63 | exports.ensureSetCookieArray = function(response) { 64 | response.headers = response.headers || {}; 65 | 66 | response.headers["Set-Cookie"] = response.headers["Set-Cookie"] || []; 67 | if (Object.prototype.toString.call(response.headers["Set-Cookie"]) === "[object String]") { 68 | var existing = response.headers["Set-Cookie"]; 69 | response.headers["Set-Cookie"] = []; 70 | response.headers["Set-Cookie"].push(existing); 71 | } 72 | 73 | return response; 74 | }; 75 | 76 | function isOfMethod(method) { 77 | return function (req) { 78 | return req.method.toLowerCase() === method; 79 | } 80 | } 81 | 82 | exports.isOfMethod = isOfMethod; 83 | 84 | /** 85 | * Returns whether a request is a get. 86 | * 87 | * @param {Request} req 88 | * @returns {Boolean} 89 | */ 90 | exports.isGet = isOfMethod('get'); 91 | 92 | /** 93 | * Returns whether a request is a post. 94 | * 95 | * @param {Request} req 96 | * @returns {Boolean} 97 | */ 98 | exports.isPost = isOfMethod('post'); 99 | 100 | /** 101 | * Returns whether a request is a put. 102 | * 103 | * @param {Request} req 104 | * @returns {Boolean} 105 | */ 106 | exports.isPut = isOfMethod('put'); 107 | 108 | /** 109 | * Returns whether a request is a delete. 110 | * 111 | * @param {Request} req 112 | * @returns {Boolean} 113 | */ 114 | exports.isDel = exports.isDelete = isOfMethod('delete'); 115 | -------------------------------------------------------------------------------- /lib/view.js: -------------------------------------------------------------------------------- 1 | var 2 | path = require('path'), 3 | http = require('http'), 4 | fs = require('fs'), 5 | extname = path.extname, 6 | engines = {}, 7 | settings = {}, 8 | _ = require('underscore'), 9 | events = require('events'), 10 | async = require('async'), 11 | q = require('./q'), 12 | when = q.when, 13 | util = require('util'), 14 | readFile = q.promisify(fs.readFile, fs); 15 | 16 | // TODO: Figure out how to make this work x-platform using the nodules 'engines' overlay 17 | //watchFile = require('fileWatcher').watchFile; 18 | 19 | var ViewEngine = exports.ViewEngine = function() { 20 | events.EventEmitter.call(this); 21 | }; 22 | 23 | util.inherits(ViewEngine, events.EventEmitter); 24 | 25 | /** 26 | * @param {String} engineName name of the engine 'mustache', 'haml' 27 | * @param {String} views path to the views 28 | * @returns {ViewEngine} 29 | */ 30 | exports.viewEngine = function(engineName, views, opts) { 31 | return Object.create(new ViewEngine(), { 32 | engineName: { value: engineName, enumerable: true, readonly: true }, 33 | views: { value: views || path.join(setting('template directory')), enumerable: true, readonly: true }, 34 | viewCache: { value: {} }, 35 | opts: { value: opts || { cache: false } } 36 | }); 37 | }; 38 | 39 | ViewEngine.prototype.clearCache = function() { 40 | this.viewCache = {}; 41 | }; 42 | 43 | /** 44 | * @api private 45 | */ 46 | ViewEngine.prototype.cacheView = function(view) { 47 | 48 | return when(this.read(view), function(str) { 49 | if (this.opts.cache) { 50 | this.cache(view, str); 51 | } 52 | return str; 53 | }.bind(this)); 54 | }; 55 | 56 | ViewEngine.prototype.read = function(view) { 57 | return readFile(isAbsolute(view) ? view : this.viewPath(view), 'utf8'); 58 | }; 59 | 60 | ViewEngine.prototype.cache = function(view, str) { 61 | if (str === undefined) { 62 | return this.viewCache[view]; 63 | } 64 | 65 | this.viewCache[view] = str; 66 | }; 67 | 68 | ViewEngine.prototype.viewPath = function(view) { 69 | return path.join(this.views, view); 70 | } 71 | 72 | /** 73 | * @api private 74 | */ 75 | ViewEngine.prototype.getCacher = function(view) { 76 | return this.cache.bind(this, view); 77 | } 78 | 79 | ViewEngine.prototype.respond = function(view, opts) { 80 | var viewEngine = this; 81 | 82 | opts = opts || {}; 83 | 84 | return q.when(viewEngine.render(view, opts), function(str) { 85 | var headers = _.extend({}, { 86 | 'content-type': 'text/html', 87 | 'content-length': Buffer.byteLength(str, 'utf-8') 88 | }, opts.headers); 89 | return { 90 | status: opts.status || 200, 91 | headers: headers, 92 | body: [str] 93 | }; 94 | }); 95 | }; 96 | 97 | ViewEngine.prototype.render = function(view, opts) { 98 | opts = opts || {}; 99 | 100 | opts.locals = opts.locals || {}; 101 | 102 | var 103 | self = this, 104 | ext = extname(view), 105 | engine = this.engineName, 106 | renderer = engine ? exports.viewEngine.engine(engine) : exports.viewEngine.engine(ext.substring(1)), 107 | layout = opts.layout === undefined ? true : opts.layout; 108 | 109 | layout = layout === true ? 'layout' + ext : layout; 110 | 111 | return when(this.cache(view) || this.cacheView(view), 112 | function success(template) { 113 | var renderedView 114 | , cacher 115 | , jsName = setting('shared js name') || 'javascript' 116 | , namespace = setting('shared js namespace') || 'bogart'; 117 | 118 | if (renderer === undefined || renderer === null) { 119 | throw new Error('Rendering engine not found for '+engine+'. Try `npm install bogart-'+engine+'`.'); 120 | } 121 | 122 | if (opts._renderedShared !== true) { 123 | if (opts.locals[jsName]) { 124 | throw 'The locals property of `render` method options contains a property named "'+jsName+'" already. ' + 125 | 'The view engine settings specify this name for the shared JavaScript. Please either change the "shared js name" settings ' + 126 | 'by executing `var view = require("view"); view.setting("shared js name", "myNewName") or remove the code that is setting ' + 127 | 'this property on the opts.locals.'; 128 | } 129 | 130 | opts.locals[jsName] = ''; 136 | 137 | opts._renderedShared = true; 138 | } 139 | 140 | self.emit('beforeRender', self, opts, template, cacher); 141 | 142 | cacher = self.getCacher(view); 143 | renderedView = renderer(template, opts, cacher, self); 144 | 145 | self.emit('afterRender', self, opts, template, cacher); 146 | 147 | return when(renderedView, function(renderedView) { 148 | if (layout) { 149 | opts.locals.body = renderedView; 150 | opts.layout = false; 151 | 152 | return self.render(layout, opts); 153 | } 154 | 155 | return renderedView; 156 | }); 157 | }); 158 | }; 159 | 160 | ViewEngine.prototype.partial = function(view, opts) { 161 | opts = opts || {}; 162 | 163 | opts.locals = opts.locals || {}; 164 | 165 | opts.partial = true; 166 | opts.layout = false; 167 | 168 | return this.render(view, opts); 169 | }; 170 | 171 | /** 172 | * Share JavaScript with the client side. 173 | */ 174 | ViewEngine.prototype.share = function(obj, name) { 175 | if (obj === undefined || obj === null) { 176 | throw 'Have to share something! First parameter to `ViewEngine.prototype.share` cannot be empty.'; 177 | } 178 | 179 | this._shared = this._shared || {}; 180 | this._shared[name] = this._shared[name] || []; 181 | 182 | if (typeof obj === 'string') { 183 | this._shared[name].push(obj); 184 | } else { 185 | this.share(stringify(obj), name); 186 | } 187 | 188 | return this; 189 | }; 190 | 191 | /** 192 | * Inspect JavaScript that is shared with the client side. 193 | * 194 | * @params {String} name The key of the JavaScript to look up. 195 | * @returns {String} JavaScript shared with the client for the `name` specified. 196 | */ 197 | ViewEngine.prototype.shared = function(name) { 198 | this._shared = this._shared || {}; 199 | return this._shared[name].join('\n'); 200 | }; 201 | 202 | /** 203 | * Add a template engine 204 | * 205 | * @param {String} engineName The name of the template engine 206 | * @param {Function} render Render a template given the templaet as a string and and options object 207 | * 208 | * @returns undefined 209 | */ 210 | exports.viewEngine.addEngine = function(engineName, render) { 211 | engines[engineName] = render; 212 | }; 213 | 214 | /** 215 | * Remove a template engine 216 | * 217 | * @param {String} engineName The name of the template engine 218 | * @returns undefined 219 | */ 220 | exports.viewEngine.removeEngine = function(engineName) { 221 | delete engines[engineName]; 222 | }; 223 | 224 | /** 225 | * Retrieves the render function for a registered template engine 226 | * 227 | * @param {String} engineName The name of the template engine 228 | * @returns {Function} Function registered to render templates of the type specified by engineName 229 | */ 230 | exports.viewEngine.engine = function(engineName) { 231 | return engines[engineName]; 232 | }; 233 | 234 | exports.viewEngine.addEngine('mustache', function(str, opts, cache, viewEngine) { 235 | var partials = {}; 236 | 237 | opts = _.extend({}, opts); 238 | 239 | if (opts.partials) { 240 | var defer = q.defer(); 241 | 242 | async.forEach(Object.keys(opts.partials), function(k, callback) { 243 | 244 | when(viewEngine.cache(opts.partials[k]) || viewEngine.cacheView(opts.partials[k]), function(str) { 245 | partials[k] = str; 246 | callback(); 247 | }); 248 | }, function(err) { 249 | delete opts.partials; 250 | 251 | if (err) { 252 | defer.reject(err); 253 | } else { 254 | defer.resolve(require('mustache').to_html(str, opts.locals, partials)); 255 | } 256 | }); 257 | 258 | return defer.promise; 259 | } 260 | 261 | return require('mustache').to_html(str, opts.locals); 262 | }); 263 | 264 | var setting = exports.setting = function(key, val) { 265 | if (val !== undefined) { 266 | settings[key] = val; 267 | return this; 268 | } 269 | 270 | return settings[key]; 271 | }; 272 | 273 | /** 274 | * Returns whether a path is an absolute path. 275 | */ 276 | function isAbsolute(path) { 277 | var windowsDriveRe = /^[a-zA-Z]:/; 278 | 279 | return path[0] === '/' || windowsDriveRe.test(path); 280 | } 281 | 282 | /** 283 | * Returns a string representation of an object. 284 | * This method is used for sharing objects with the client. 285 | * 286 | * @param {Object} obj An object. 287 | * @returns {String} a string representation of the object. 288 | * @api private 289 | */ 290 | function stringify(obj) { 291 | if (Array.isArray(obj)) { 292 | return '['+obj.map(stringify).join(',')+']'; 293 | } else if (obj instanceof Date) { 294 | return 'new Date("'+obj.toString()+'")'; 295 | } else if (typeof obj === 'function') { 296 | return obj.toString(); 297 | } else { 298 | return JSON.stringify(obj); 299 | } 300 | } 301 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bogart", 3 | "description": "Fast JSGI web framework taking inspiration from Sinatra", 4 | "version": "0.5.27", 5 | "keywords": ["bogart", "framework", "sinatra", "REST"], 6 | "author": "Nathan Stott ", 7 | "email": "nathan.stott@whiteboard-it.com", 8 | "homepage": "https://github.com/nrstott/bogart", 9 | "main": "./lib/bogart", 10 | "directories": { "lib": "./lib" }, 11 | "repository": { 12 | "type": "git", 13 | "url": "https://github.com/nrstott/bogart" 14 | }, 15 | "bugs": { 16 | "url": "https://github.com/nrstott/bogart/issues", 17 | "email": "nrstott@gmail.com" 18 | }, 19 | "dependencies": { 20 | "q": "0.9.6", 21 | "q-io": "~1.13.1", 22 | "jsgi": ">=v0.2.2", 23 | "mustache": "0.7.2", 24 | "underscore": ">=1.4.4", 25 | "node-uuid": ">=1.2.0", 26 | "parted": "git://github.com/soitgoes/parted.git#v0.1.4", 27 | "oauth": "=0.9.5", 28 | "request": "=2.2.9", 29 | "commander": "*", 30 | "mkdirp": "*", 31 | "async": "0.1.22", 32 | "sinon": "1.5.2" 33 | }, 34 | "devDependencies": { 35 | "haml": ">=0.4.2", 36 | "jade": ">=0.15.4", 37 | "jasmine-node": ">=1.4.0" 38 | }, 39 | "scripts": { 40 | "test": "jasmine-node --coffee spec/" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /spec/appSpec.coffee: -------------------------------------------------------------------------------- 1 | bogart = require '../lib/bogart' 2 | 3 | describe 'bogart app', -> 4 | app = null 5 | 6 | beforeEach -> 7 | app = bogart.app() 8 | 9 | describe 'given a router with no parameters', -> 10 | 11 | it 'should not be started', -> 12 | expect(app.started).toBe(false) 13 | 14 | it 'should have `use` method', -> 15 | expect(typeof app.use).toBe('function') 16 | 17 | describe 'afterAddMiddleware event', -> 18 | appParam = null 19 | middlewareParam = null 20 | router = null 21 | 22 | beforeEach -> 23 | router = bogart.router() 24 | 25 | app.on 'afterAddMiddleware', (app, middleware) -> 26 | appParam = app 27 | middlewareParam = middleware 28 | 29 | app.use router 30 | 31 | it 'should raise afterAddMiddleware with correct app parameters', -> 32 | expect(appParam).toBe(app) 33 | 34 | it 'should raise afterAddMiddleware with correct middleware parameter', -> 35 | expect(middlewareParam).toBe(router) 36 | 37 | describe 'starting the app', -> 38 | server = {} 39 | result = null 40 | 41 | beforeEach -> 42 | spyOn(bogart, 'start').andReturn server 43 | 44 | app.use(-> 45 | return (req) -> 46 | bogart.html 'Hello World' 47 | ) 48 | 49 | result = app.start() 50 | 51 | it 'should be started', -> 52 | expect(app.started).toBe(true) 53 | 54 | it 'should have been called with correct jsgi parameters', -> 55 | expect(bogart.start).toHaveBeenCalledWith(jasmine.any(Function), { port: 8080, host: '127.0.0.1' }) 56 | 57 | it 'should return server', -> 58 | expect(result).toBe(server) 59 | 60 | describe 'given JSGI options', -> 61 | router = null 62 | server = {} 63 | jsgiOpts = null 64 | 65 | beforeEach -> 66 | spyOn(bogart, 'start').andReturn server 67 | 68 | router = bogart.router() 69 | app.use router 70 | 71 | jsgiOpts = { port: 1337, host: 'whiteboard-it.com', somethingElse: 'anotherOption' } 72 | app.start jsgiOpts 73 | 74 | it 'should be started', -> 75 | expect(app.started).toBe(true) 76 | 77 | it 'should have been started with correct JSGI Options', -> 78 | expect(bogart.start).toHaveBeenCalledWith jasmine.any(Function), jsgiOpts 79 | 80 | -------------------------------------------------------------------------------- /spec/fixtures/chrome.part: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nrstott/bogart/a6e3b01b2e6f1247dde697038101fd6fa94e76b0/spec/fixtures/chrome.part -------------------------------------------------------------------------------- /spec/fixtures/greeting.mustache: -------------------------------------------------------------------------------- 1 |

With Partial

-------------------------------------------------------------------------------- /spec/fixtures/index.mustache: -------------------------------------------------------------------------------- 1 |

Hello World from Mustache

-------------------------------------------------------------------------------- /spec/fixtures/partial-test.mustache: -------------------------------------------------------------------------------- 1 |

Hello World from Mustache

{{>greeting}} -------------------------------------------------------------------------------- /spec/fixtures/test.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nrstott/bogart/a6e3b01b2e6f1247dde697038101fd6fa94e76b0/spec/fixtures/test.jpg -------------------------------------------------------------------------------- /spec/fixtures/text.txt: -------------------------------------------------------------------------------- 1 | Hello World -------------------------------------------------------------------------------- /spec/helpers/JsgiRequestHelper.coffee: -------------------------------------------------------------------------------- 1 | _ = require 'underscore' 2 | 3 | class MockJsgiRequest 4 | this.root = () -> 5 | new RootRequest() 6 | 7 | constructor: (@pathInfo, @method = 'get', @headers = {}) -> 8 | 9 | class RootRequest extends MockJsgiRequest 10 | constructor: (method, headers) -> 11 | super('/', method, headers) 12 | 13 | module.exports = MockJsgiRequest -------------------------------------------------------------------------------- /spec/middleware/facebookSpec.coffee: -------------------------------------------------------------------------------- 1 | facebook = require '../../lib/middleware/facebook' 2 | 3 | describe 'parse facebook profile', -> 4 | rawProfile = null 5 | parsedProfile = null 6 | 7 | shouldMap = (propertyName, rawValue, parsedValue) -> 8 | it "should have correct #{propertyName}", -> 9 | expect(parsedValue()).toBe(rawValue()) 10 | 11 | beforeEach -> 12 | rawProfile = 13 | id: 'abc123', 14 | username: 'myusername', 15 | name: 'myname', 16 | last_name: 'last_name', 17 | first_name: 'first_name', 18 | middle_name: 'middle_name', 19 | gender: 'M', 20 | link: 'http://me.facebook.com', 21 | email: 'hello@test.com' 22 | 23 | parsedProfile = facebook.parseFacebookProfile JSON.stringify(rawProfile) 24 | 25 | shouldMap 'id', (() -> rawProfile.id), (() -> parsedProfile.id) 26 | 27 | shouldMap 'username', (() -> rawProfile.username), (() -> parsedProfile.username) 28 | 29 | shouldMap 'name', (() -> rawProfile.name), (() -> parsedProfile.displayName) 30 | 31 | shouldMap 'last_name', (() -> rawProfile.last_name), (() -> parsedProfile.name.familyName) 32 | 33 | shouldMap 'first_name', (() -> rawProfile.first_name), (() -> parsedProfile.name.givenName) 34 | 35 | shouldMap 'middle_name', (() -> rawProfile.middle_name), (() -> parsedProfile.name.middleName) 36 | 37 | shouldMap 'gender', (() -> rawProfile.gender), (() -> parsedProfile.gender) 38 | 39 | shouldMap 'profileUrl', (() -> rawProfile.link), (() -> parsedProfile.profileUrl) 40 | 41 | shouldMap 'email', (() -> rawProfile.email), (() -> parsedProfile.emails[0]) 42 | 43 | -------------------------------------------------------------------------------- /spec/middleware/session/cookieDataProviderSpec.coffee: -------------------------------------------------------------------------------- 1 | JsgiRequest = require '../../helpers/JsgiRequestHelper' 2 | CookieDataProvider = require '../../../lib/middleware/session/cookieDataProvider' 3 | uuid = require 'node-uuid' 4 | q = require '../../../lib/q' 5 | 6 | describe 'Cookie Data Provider', -> 7 | 8 | describe 'load session', -> 9 | cookieDataProvider = null 10 | sessionId = null 11 | encryptionKey = null 12 | decryptedSessionData = null 13 | session = null 14 | 15 | beforeEach -> 16 | encryptionKey = uuid() 17 | sessionId = 1293 18 | 19 | req = new JsgiRequest('/', 'get', { cookie: 'bogart_session_data={}' }) 20 | 21 | encrypt = jasmine.createSpy 'encrypt' 22 | decrypt = jasmine.createSpy 'decrypt' 23 | 24 | encrypt.andReturn 'bogart_session_data' 25 | 26 | decryptedSessionData = '{}' 27 | decrypt.andReturn decryptedSessionData 28 | 29 | cookieDataProvider = new CookieDataProvider({ 30 | secret: encryptionKey, 31 | encrypt: encrypt, 32 | decrypt: decrypt 33 | }); 34 | 35 | session = cookieDataProvider.loadSession req, sessionId 36 | 37 | it 'should call encrypt', (done) -> 38 | q.when session, (session) -> 39 | expect(cookieDataProvider.encrypt).toHaveBeenCalled() 40 | done() 41 | 42 | it 'should call decrypt', (done) -> 43 | q.when session, (session) -> 44 | expect(cookieDataProvider.decrypt).toHaveBeenCalled() 45 | done() 46 | 47 | it 'should have correct session data', (done) -> 48 | q.when session, (session) -> 49 | expect(session).toEqual JSON.parse(decryptedSessionData) 50 | done() 51 | 52 | describe 'destroy session', -> 53 | res = null 54 | sessionId = null 55 | encrypt = null 56 | 57 | beforeEach -> 58 | sessionId = '--some-session-id--' 59 | 60 | encrypt = jasmine.createSpy 'encrypt' 61 | encrypt.andCallFake (sessionId, secret) -> 62 | sessionId 63 | 64 | cookieDataProvider = new CookieDataProvider 65 | secret: 'VERY_SECRET', 66 | encrypt: encrypt 67 | 68 | req = new JsgiRequest('/', 'get', { cookie: '' }) 69 | req.env = 70 | session: 71 | foo: 'bar' 72 | bar: 'baz' 73 | 74 | res = { headers: {} } 75 | res = cookieDataProvider.destroy(req, res, sessionId) 76 | 77 | it 'should call encrypt', (done) -> 78 | q(res) 79 | .then -> 80 | expect(encrypt).toHaveBeenCalled() 81 | .fail (err) => 82 | @fail err 83 | .fin done 84 | 85 | it 'should have empty session cookie', (done) -> 86 | q(res) 87 | .then (res) -> 88 | cookie = res.headers['Set-Cookie'][0] 89 | parts = cookie.split(';') 90 | val = parts[0].split('=')[1] 91 | expect(val).toEqual(encodeURIComponent(JSON.stringify({}))) 92 | .fail (err) => 93 | @fail err 94 | .fin done 95 | -------------------------------------------------------------------------------- /spec/middlewareSpec.coffee: -------------------------------------------------------------------------------- 1 | bogart = require '../lib/bogart' 2 | q = bogart.q 3 | mockRequest = require './helpers/JsgiRequestHelper' 4 | 5 | describe 'middleware helper', -> 6 | testMiddleware = null 7 | req = null 8 | next = null 9 | handler = null 10 | 11 | beforeEach -> 12 | handler = jasmine.createSpy() 13 | 14 | testMiddleware = bogart.middleware handler 15 | req = mockRequest.root() 16 | next = jasmine.createSpy() 17 | 18 | testMiddleware(next)(req) 19 | 20 | it 'should be a function', -> 21 | expect(typeof testMiddleware).toBe 'function' 22 | 23 | it 'should call handler with correct request', -> 24 | expect(handler).toHaveBeenCalledWith req, jasmine.any(Function) 25 | 26 | it 'should call handler with correct nextApp', -> 27 | expect(handler).toHaveBeenCalledWith jasmine.any(Object), next 28 | 29 | 30 | describe 'parse json', -> 31 | parseJsonMiddleware = null 32 | body = null 33 | jsgiRequest = null 34 | reqParam = null 35 | res = null 36 | 37 | beforeEach -> 38 | forEachDeferred = q.defer() 39 | headers = { 'content-type': 'application/json' } 40 | 41 | body = { 42 | forEach: (callback) -> 43 | callback(JSON.stringify { a: '1' }) 44 | forEachDeferred.promise 45 | } 46 | 47 | request = { headers: headers, body: body } 48 | 49 | parseJsonMiddleware = bogart.middleware.parseJson((req) -> 50 | reqParam = req 51 | ) 52 | 53 | res = parseJsonMiddleware(request) 54 | 55 | process.nextTick(() -> 56 | forEachDeferred.resolve() 57 | ) 58 | 59 | it 'should have passed request to nextApp', (done) -> 60 | q.when(res, () -> 61 | expect(reqParam).toBeDefined() 62 | done() 63 | , (err) => 64 | this.fail err 65 | done() 66 | ) 67 | 68 | it 'should have correct body', (done) -> 69 | q.when(res, () -> 70 | expect(reqParam.body.a).toBe '1' 71 | done() 72 | , (err) => 73 | this.fail err 74 | done() 75 | ) 76 | 77 | describe 'test parse form', -> 78 | forEachDeferred = null 79 | parseFormMiddleware = null 80 | echoApp = null 81 | res = null 82 | reqParamInNextApp = null 83 | 84 | beforeEach -> 85 | echoApp = () -> 86 | (req) -> 87 | reqParamInNextApp = req 88 | { body: [], headers: {}, status: 200 } 89 | 90 | forEachDeferred = q.defer() 91 | 92 | parseFormMiddleware = bogart.middleware.parseForm echoApp() 93 | 94 | jsgiRequest = { 95 | headers: { 'content-type': 'application/x-www-form-urlencoded' }, 96 | body: { 97 | forEach: (callback) -> 98 | callback('a=1') 99 | } 100 | } 101 | 102 | res = parseFormMiddleware jsgiRequest 103 | 104 | process.nextTick(() -> 105 | forEachDeferred.resolve() 106 | ) 107 | 108 | it 'should pass correct req to nextApp', (done) -> 109 | q.when(res, () -> 110 | expect(reqParamInNextApp.body.a).toBe '1' 111 | done() 112 | , (err) => 113 | this.fail(err) 114 | done() 115 | ) 116 | 117 | describe 'method override', -> 118 | methodOverrideMiddleware = null 119 | jsgiRequest = null 120 | res = null 121 | 122 | beforeEach -> 123 | methodOverrideMiddleware = bogart.middleware.methodOverride((req) -> 124 | {} 125 | ) 126 | 127 | headers = { 'content-type': 'text/html' } 128 | jsgiRequest = { method: 'post', env: {}, body: { _method: 'put' }, headers: headers } 129 | 130 | res = methodOverrideMiddleware jsgiRequest 131 | 132 | it 'should change request method to `PUT`', (done) -> 133 | q.when(res, () -> 134 | expect(jsgiRequest.method).toBe 'PUT' 135 | done() 136 | , (err) => 137 | this.fail(err) 138 | done() 139 | ) 140 | 141 | describe 'gzip', -> 142 | jsgiRequest = null 143 | res = null 144 | 145 | beforeEach -> 146 | headers = { 'content-type': 'text/html', 'accept-encoding': 'gzip' } 147 | jsgiRequest = { method: 'post', env: {}, headers: headers, body: [] } 148 | 149 | gzipMiddleware = bogart.middleware.gzip (req) -> 150 | bogart.html 'Hello World' 151 | 152 | res = gzipMiddleware jsgiRequest 153 | 154 | it 'should have response body', (done) -> 155 | q.when(res, (res) -> 156 | expect(typeof res.body).toBe 'object' 157 | done() 158 | , (err) => 159 | this.fail(err) 160 | done() 161 | ) 162 | 163 | it 'should have correct content-type', (done) -> 164 | q.when(res, (res) -> 165 | expect(res.headers['content-type']).toBe 'text/html' 166 | done() 167 | , (err) => 168 | this.fail(err); 169 | done(); 170 | ) 171 | 172 | it 'should have correct status', (done) -> 173 | q.when(res, (res) -> 174 | expect(res.status).toBe 200 175 | done() 176 | , (err) => 177 | this.fail(err) 178 | done() 179 | ) 180 | 181 | describe 'error middleware given exception', -> 182 | res = null 183 | errorMessage = '__Intentional Error Test__' 184 | 185 | beforeEach -> 186 | errorMiddleware = bogart.middleware.error { logError: false }, (req) -> 187 | throw new Error(errorMessage) 188 | 189 | res = errorMiddleware { method: 'get', env: {}, headers: {}, body: [] } 190 | 191 | it 'should have correct status', (done) -> 192 | q.when res, (res) -> 193 | expect(res.status).toBe 500 194 | done() 195 | 196 | it 'should have correct content-type', (done) -> 197 | q.when res, (res) -> 198 | expect(res.headers['content-type']).toBe 'text/html' 199 | done() 200 | 201 | it 'should contain error message', (done) -> 202 | q.when res, (res) -> 203 | expect(res.body.join('')).toContain(errorMessage) 204 | done() 205 | 206 | describe 'error middleware rejected promise given string', -> 207 | rejectionMessage = '__INTENTIONAL REJECTION__' 208 | res = null 209 | 210 | beforeEach -> 211 | errorMiddleware = bogart.middleware.error { logError: false }, (req) -> 212 | q.reject rejectionMessage 213 | 214 | res = errorMiddleware { body: [], headers: {}, env: {} } 215 | 216 | it 'should have correct status', (done) -> 217 | q.when res, (res) -> 218 | expect(res.status).toBe 500 219 | done() 220 | 221 | it 'should have correct content-type', (done) -> 222 | q.when res, (res) -> 223 | expect(res.headers['content-type']).toBe 'text/html' 224 | done() 225 | 226 | it 'should contain rejection message', (done) -> 227 | q.when res, (res) -> 228 | expect(res.body.join '').toContain rejectionMessage 229 | done() 230 | 231 | describe 'error middleware rejected promise given Error', -> 232 | errorMessage = '__INTENTIONAL ERROR__' 233 | rejectionError = new Error errorMessage 234 | res = null 235 | 236 | beforeEach -> 237 | errorMiddleware = bogart.middleware.error { logError: false }, (req) -> 238 | q.reject rejectionError 239 | 240 | res = errorMiddleware { body: [], headers: {}, env: {} } 241 | 242 | it 'should have correct status', (done) -> 243 | q.when res, (res) -> 244 | expect(res.status).toBe 500 245 | done() 246 | 247 | it 'should have correct content-type', (done) -> 248 | q.when res, (res) -> 249 | expect(res.headers['content-type']).toBe 'text/html' 250 | done() 251 | 252 | it 'should contain error message', (done) -> 253 | q.when res, (res) -> 254 | expect(res.body.join '').toContain errorMessage 255 | done() 256 | 257 | describe 'flash middleware', -> 258 | res = null 259 | foo = null 260 | jsgiRequest = null 261 | flashMiddleware = null 262 | 263 | beforeEach -> 264 | flashMiddleware = bogart.middleware.flash {}, (req) -> 265 | req.flash 'foo', 'bar' 266 | 267 | foo = req.flash 'foo' 268 | 269 | { status: 200, body: [], headers: { 'content-type': 'text/html' } } 270 | 271 | jsgiRequest = { headers: { 'content-type': 'text/plain' }, body: [] } 272 | 273 | res = flashMiddleware(jsgiRequest).then (res) -> 274 | cookieStr = res.headers['Set-Cookie'].join('').replace(/;$/, ''); 275 | jsgiRequest.headers.cookie = cookieStr 276 | res 277 | 278 | describe 'first request', -> 279 | 280 | it 'should have `foo` value of undefined', (done) -> 281 | q.when res, (res) -> 282 | expect(foo).toEqual undefined 283 | done() 284 | 285 | describe 'second request', -> 286 | 287 | beforeEach -> 288 | res = q.when res, () -> 289 | flashMiddleware jsgiRequest 290 | 291 | it 'should have correct `foo` value', (done) -> 292 | q.when res, (res) -> 293 | expect(foo).toEqual 'bar' 294 | done() 295 | 296 | describe 'parted', -> 297 | partedMiddleware = null 298 | res = null 299 | 300 | beforeEach -> 301 | partedMiddleware = bogart.middleware.parted (req) -> 302 | req 303 | 304 | describe 'json', -> 305 | 306 | beforeEach -> 307 | res = partedMiddleware { 308 | headers: { 'content-type': 'application/json' }, 309 | body: [ '{ "hello": "world" }' ], 310 | env: {}, 311 | method: 'POST' 312 | } 313 | 314 | it 'should have correct body', (done) -> 315 | q.when res, (req) -> 316 | expect(req.body.hello).toBe 'world' 317 | done() 318 | 319 | describe 'form url-encoded', -> 320 | 321 | beforeEach -> 322 | body = { 323 | forEach: (callback) -> 324 | callback('hello=world') 325 | } 326 | 327 | res = partedMiddleware { 328 | method: 'POST', 329 | env: {}, 330 | headers: { 'content-type': 'application/x-www-form-urlencoded' }, 331 | body: body 332 | } 333 | 334 | it 'should have correct body', (done) -> 335 | q.when res, (res) -> 336 | expect(res.body.hello).toBe 'world' 337 | done() 338 | 339 | describe 'multipart form-encoded', -> 340 | 341 | beforeEach -> 342 | res = partedMiddleware multipartRequest(100, 'chrome') 343 | 344 | it 'should have content', (done) -> 345 | q.when res, (res) -> 346 | expect(res.body.content).not.toBe undefined 347 | done() 348 | 349 | describe 'session', -> 350 | sessionApp = null 351 | firstRequest = true 352 | res = null 353 | values = [] 354 | 355 | beforeEach -> 356 | sessionApp = bogart.middleware.session { secret: 'my-super-secret' }, (req) -> 357 | req.session('foo', 'bar') if firstRequest 358 | firstRequest = false 359 | 360 | values.push req.session('foo') 361 | 362 | return { 363 | status: 200, 364 | body: [] 365 | } 366 | 367 | headers = { 'content-type': 'text/plain' } 368 | jsgiRequest = { headers: headers, body: [] } 369 | 370 | res = q.when sessionApp(jsgiRequest), (res) -> 371 | cookieStr = res.headers['Set-Cookie'].join('').replace(/;$/, ''); 372 | 373 | jsgiRequest.headers.cookie = cookieStr 374 | 375 | q.when sessionApp(jsgiRequest) 376 | 377 | it 'should have correct value for first request', (done) -> 378 | q.when res, (res) -> 379 | expect(values[0]).toBe 'bar' 380 | done() 381 | , (err) => 382 | this.fail err 383 | 384 | it 'should have correct value for second request', (done) -> 385 | q.when res, (res) -> 386 | expect(values[1]).toBe 'bar' 387 | done() 388 | , (err) => 389 | this.fail err 390 | 391 | describe 'validate response middleware', -> 392 | validateResApp = null 393 | 394 | describe 'null response', -> 395 | 396 | beforeEach -> 397 | validateResApp = bogart.middleware.validateResponse (req) -> 398 | null 399 | 400 | it 'should have correct error', (done) -> 401 | validateResApp().fail (err) -> 402 | expect(err).toBe 'Response must be an object.' 403 | done() 404 | , (err) => 405 | this.fail error 406 | done() 407 | 408 | describe 'response without a body', -> 409 | 410 | beforeEach -> 411 | validateResApp = bogart.middleware.validateResponse (req) -> 412 | { status: 200, headers: {} } 413 | 414 | it 'should have correct error', (done) -> 415 | validateResApp().fail (err) -> 416 | expect(err).toBe 'Response must have a body property.' 417 | done() 418 | 419 | describe 'response that has a body that is not a forEachable', -> 420 | 421 | beforeEach -> 422 | validateResApp = bogart.middleware.validateResponse (req) -> 423 | { status: 200, body: {}, headers: {} } 424 | 425 | it 'should have correct error', (done) -> 426 | validateResApp().fail (err) -> 427 | expect(err).toBe 'Response body must have a forEach method.' 428 | done() 429 | 430 | describe 'given response with body that has a forEach property that is not a function', -> 431 | 432 | beforeEach -> 433 | validateResApp = bogart.middleware.validateResponse (req) -> 434 | { status: 200, body: { forEach: {} }, headers: {} } 435 | 436 | it 'should have correct error', (done) -> 437 | validateResApp().fail (err) -> 438 | expect(err).toBe 'Response body has a forEach method but the forEach method is not a function.' 439 | done() 440 | 441 | describe 'given a response without status', -> 442 | 443 | beforeEach -> 444 | validateResApp = bogart.middleware.validateResponse (req) -> 445 | { body: [], headers: {} } 446 | 447 | it 'should have correct error', (done) -> 448 | validateResApp().fail (err) -> 449 | expect(err).toBe 'Response must have a status property.' 450 | done() 451 | 452 | describe 'given a response with a non-number status', -> 453 | 454 | beforeEach -> 455 | validateResApp = bogart.middleware.validateResponse (req) -> 456 | { body: [], headers: {}, status: '200' } 457 | 458 | it 'should have correct error', (done) -> 459 | validateResApp().fail (err) -> 460 | expect(err).toBe 'Response has a status property but the status property must be a number.' 461 | done() 462 | 463 | describe 'string return adapter', -> 464 | str = null 465 | res = null 466 | 467 | beforeEach -> 468 | str = 'This is a message' 469 | req = mockRequest.root() 470 | 471 | stringReturnAdapterApp = bogart.middleware.stringReturnAdapter -> 472 | str 473 | 474 | res = stringReturnAdapterApp req 475 | 476 | it 'should have correct body', (done) -> 477 | q.when res, (res) -> 478 | expect(res.body).toEqual([ str ]) 479 | done() 480 | 481 | describe 'batteries', -> 482 | 483 | describe 'given directory configuration', -> 484 | 485 | beforeEach -> 486 | spyOn bogart.middleware, 'directory' 487 | bogart.middleware.batteries({ directory: 'public', secret: 'my-super-secret' })((req) -> {}) 488 | 489 | it 'should pass correct configuration to directory middleware', -> 490 | expect(bogart.middleware.directory).toHaveBeenCalledWith 'public', jasmine.any(Function) 491 | 492 | describe 'given empty configuration', -> 493 | 494 | beforeEach -> 495 | spyOn bogart.middleware, 'directory' 496 | bogart.middleware.batteries({})((req) -> {}) 497 | 498 | it 'should pass default configuration for `directory`', -> 499 | expect(bogart.middleware.directory).toHaveBeenCalledWith 'public', jasmine.any(Function) 500 | 501 | ### 502 | Create a mock request 503 | Modified from the mock request method in Parted in compliance with the license. 504 | ### 505 | multipartRequest = (size, file) -> 506 | fs = require 'fs' 507 | path = require 'path' 508 | 509 | file = path.join __dirname, 'fixtures', file+'.part' 510 | 511 | stream = fs.createReadStream file, { 512 | bufferSize: size 513 | } 514 | 515 | boundary = fs 516 | .readFileSync(file) 517 | .toString('utf8') 518 | .match(/--[^\r\n]+/)[0] 519 | .slice(2) 520 | 521 | res = 522 | headers: { 'content-type': 'multipart/form-data; boundary="' + boundary + '"' } 523 | method: 'POST' 524 | env: {} 525 | pipe: (dest) -> 526 | stream.pipe dest 527 | emit: (ev, err) -> 528 | this.errback && this.errback(err) if ev == 'error' 529 | this 530 | on: (ev, func) -> 531 | this.errback = func if ev == 'error' 532 | this 533 | destroy: () -> 534 | stream.destroy() 535 | this 536 | body: 537 | forEach: (fn) -> 538 | deferred = q.defer() 539 | 540 | stream.on 'data', (data) -> 541 | fn data 542 | 543 | stream.on 'end', -> 544 | deferred.resolve() 545 | 546 | deferred.promise 547 | -------------------------------------------------------------------------------- /spec/requestSpec.coffee: -------------------------------------------------------------------------------- 1 | request = require '../lib/request' 2 | 3 | describe 'construction request given empty object parameter', -> 4 | req = null 5 | keys = null 6 | 7 | beforeEach -> 8 | req = request({}) 9 | keys = Object.keys req 10 | 11 | it 'should have `params`', -> 12 | expect(keys).toContain 'params' 13 | 14 | it 'should have `search`', -> 15 | expect(keys).toContain 'search' 16 | 17 | it 'should have `isXMLHttpRequest`', -> 18 | expect(keys).toContain 'isXMLHttpRequest' 19 | 20 | describe 'query string parameters', -> 21 | jsgiRequest = null 22 | req = null 23 | 24 | beforeEach -> 25 | jsgiRequest = { queryString: 'a=1&b=abc' } 26 | 27 | req = request jsgiRequest 28 | 29 | it 'should have correct `queryString`', -> 30 | expect(req.queryString).toBe jsgiRequest.queryString 31 | 32 | it 'should have correct `search`', -> 33 | expect(req.search).toEqual { a: '1', b: 'abc' } 34 | 35 | describe 'route parameters', -> 36 | req = null 37 | 38 | beforeEach -> 39 | req = request {} 40 | 41 | req.routeParams.name = 'Bob' 42 | 43 | it 'should have correct name parameter', -> 44 | expect(req.params.name).toBe 'Bob' 45 | 46 | it 'should have correct name search parameter', -> 47 | expect(req.search.name).not.toBe 'Bob' 48 | 49 | 50 | describe 'XHR request', -> 51 | req = null 52 | 53 | beforeEach -> 54 | req = request { headers: { 'x-requested-with': '' } } 55 | 56 | it 'should have correct `isXMLHttpRequest`', -> 57 | expect(req.isXMLHttpRequest).toBe true 58 | 59 | describe 'non-XHR request', -> 60 | req = null 61 | 62 | beforeEach -> 63 | req = request { headers: {} } 64 | 65 | it 'should have correct `isXMLHttpRequest`', -> 66 | expect(req.isXMLHttpRequest).toBe false 67 | -------------------------------------------------------------------------------- /spec/responseBuilderSpec.coffee: -------------------------------------------------------------------------------- 1 | bogart = require '../lib/bogart' 2 | q = bogart.q 3 | fs = require 'fs' 4 | path = require 'path' 5 | 6 | describe 'resolving ResponseBuilder', -> 7 | responseBuilder = null 8 | resolveVal = { 'hello': 'world' } 9 | 10 | beforeEach -> 11 | responseBuilder = new bogart.ResponseBuilder() 12 | 13 | setTimeout -> 14 | responseBuilder.resolve resolveVal 15 | 16 | it 'should resolve', (done) -> 17 | q.when responseBuilder, (res) -> 18 | expect(res).toBe resolveVal 19 | done() 20 | 21 | describe 'rejecting ResponseBuilder', -> 22 | responseBuilder = null 23 | rejectVal = null 24 | 25 | beforeEach -> 26 | rejectVal = new Error('something bad happened') 27 | 28 | responseBuilder = new bogart.ResponseBuilder() 29 | 30 | setTimeout -> 31 | responseBuilder.reject(rejectVal) 32 | 33 | it 'should reject', (done) -> 34 | q.when(responseBuilder).fail (err) -> 35 | expect(err).toBe rejectVal 36 | done() 37 | 38 | describe 'sending a string', -> 39 | responseBuilder = null 40 | msg = 'Hello World' 41 | 42 | beforeEach -> 43 | responseBuilder = bogart.ResponseBuilder() 44 | 45 | responseBuilder.send msg 46 | 47 | responseBuilder.end() 48 | 49 | it 'should have correct body', (done) -> 50 | q.when responseBuilder, (res) -> 51 | written = '' 52 | 53 | q.when res.body.forEach((data) -> 54 | written += data 55 | ), -> 56 | expect(written).toBe msg 57 | done() 58 | 59 | describe 'sending a buffer', -> 60 | responseBuilder = null 61 | msg = null 62 | buffer = null 63 | 64 | beforeEach -> 65 | msg = 'Hello World' 66 | buffer = new Buffer msg.length 67 | buffer.write msg 68 | 69 | responseBuilder = new bogart.ResponseBuilder() 70 | responseBuilder.send buffer 71 | responseBuilder.end() 72 | 73 | it 'should have correct body', (done) -> 74 | q.when responseBuilder, (res) -> 75 | written = '' 76 | 77 | forEachFinished = res.body.forEach (data) -> 78 | written += data 79 | 80 | q.when forEachFinished, -> 81 | expect(written).toBe msg 82 | done() 83 | 84 | describe 'sending binary', -> 85 | responseBuilder = null 86 | filePath = path.join __dirname, 'fixtures', 'test.jpg' 87 | stat = fs.statSync filePath 88 | fileContent = null 89 | 90 | beforeEach -> 91 | responseBuilder = new bogart.ResponseBuilder() 92 | 93 | fs.readFile filePath, 'binary', (err, content) => 94 | this.fail err if err 95 | 96 | fileContent = content 97 | responseBuilder.send content 98 | responseBuilder.end() 99 | 100 | it 'should have correct body', (done) -> 101 | q.when responseBuilder, (res) -> 102 | written = new Buffer stat.size 103 | 104 | forEachFinished = res.body.forEach (data) -> 105 | written.write data, 'binary' 106 | 107 | q.when forEachFinished, -> 108 | expect(written.toString 'binary').toBe fileContent 109 | done() 110 | 111 | -------------------------------------------------------------------------------- /spec/responseHelperSpec.coffee: -------------------------------------------------------------------------------- 1 | bogart = require '../lib/bogart' 2 | q = bogart.q 3 | path = require 'path' 4 | fs = require 'fs' 5 | 6 | describe 'json', -> 7 | it 'should have default status of 200', -> 8 | expect(bogart.json({}).status).toBe 200 9 | 10 | it 'given status should override status', -> 11 | expect(bogart.json({}, { status: 403 }).status).toBe 403 12 | 13 | describe 'given object', -> 14 | res = null 15 | obj = null 16 | etag = "298fjsdkf" 17 | 18 | beforeEach -> 19 | obj = { hello: 'world' } 20 | opts = { headers: {ETag:etag} } 21 | 22 | res = bogart.json obj, opts 23 | 24 | it 'should have correct body', -> 25 | expect(res.body.join()).toBe JSON.stringify(obj) 26 | 27 | it 'should add any additional headers', -> 28 | expect(res.headers["ETag"]).toBe etag 29 | 30 | 31 | describe 'cors', -> 32 | it 'should have default status of 200', -> 33 | expect(bogart.cors({}).status).toBe 200 34 | 35 | it 'given status should override status', -> 36 | expect(bogart.cors({}, { status: 403 }).status).toBe 403 37 | 38 | describe 'given object', -> 39 | res = null 40 | body = null 41 | 42 | beforeEach -> 43 | body = { hello: 'world' } 44 | 45 | res = bogart.cors body 46 | 47 | it 'should have correct body', -> 48 | expect(res.body.join()).toBe JSON.stringify(body) 49 | it 'should have correct headers', -> 50 | expect(res.headers['Access-Control-Allow-Origin']).toBe '*' 51 | expect(res.headers['Access-Control-Allow-Methods']).toBe 'GET,PUT,POST,DELETE' 52 | expect(res.headers['Access-Control-Allow-Headers']).toBe 'x-requested-with,*' 53 | it 'given headers should override the default headers', -> 54 | headers = {'Access-Control-Allow-Origin': 'http://whiteboard-it.com', 'Content-Type': 'text/plain'} 55 | expect(bogart.cors({a:'b'},{headers: headers}).headers['Access-Control-Allow-Origin']).toBe 'http://whiteboard-it.com' 56 | expect(bogart.cors({a:'b'},{headers: headers}).headers['Content-Type']).toBe 'text/plain' 57 | 58 | describe 'error', -> 59 | it 'should have default status of 500', -> 60 | expect(bogart.error().status).toBe 500 61 | 62 | it 'should override status', -> 63 | expect(bogart.error('', { status: 403 }).status).toBe 403 64 | 65 | 66 | describe 'html', -> 67 | it 'should have default status of 200', -> 68 | expect(bogart.html('hello').status).toBe 200 69 | 70 | it 'should override status', -> 71 | expect(bogart.html('hello', { status: 404 }).status).toBe 404 72 | 73 | describe 'given html string', -> 74 | res = null 75 | str = null 76 | 77 | beforeEach -> 78 | str = '

Hello

' 79 | res = bogart.html str 80 | 81 | it 'should have correct content-type', -> 82 | expect(res.headers['content-type']).toBe 'text/html' 83 | 84 | it 'should have correct content-length', -> 85 | expect(res.headers['content-length']).toBe Buffer.byteLength(str, 'utf-8') 86 | 87 | it 'should have correct body', -> 88 | expect(res.body.join()).toBe str 89 | 90 | 91 | describe 'pipe', -> 92 | res = null 93 | 94 | beforeEach -> 95 | readStream = fs.createReadStream path.join(__dirname, 'fixtures', 'text.txt') 96 | res = bogart.pipe readStream 97 | 98 | it 'should have correct body', (done) -> 99 | q.when(res, (res) -> 100 | written = '' 101 | 102 | forEachFinished = res.body.forEach (data) -> 103 | written += data 104 | 105 | q.when forEachFinished, -> 106 | expect(written).toBe 'Hello World' 107 | done() 108 | ).fail (err) => 109 | this.fail err 110 | done() 111 | 112 | describe 'pipe given forEachable', -> 113 | forEachable = null 114 | deferred = null 115 | msg = null 116 | res = null 117 | 118 | beforeEach -> 119 | msg = 'Hello World' 120 | 121 | deferred = q.defer() 122 | 123 | forEachable = { 124 | forEach: (callback) -> 125 | callback msg 126 | deferred.promise 127 | } 128 | 129 | setTimeout -> 130 | deferred.resolve() 131 | 132 | res = bogart.pipe forEachable 133 | 134 | it 'should have correct body', (done) -> 135 | q.when(res, (res) -> 136 | written = '' 137 | 138 | forEachFinished = res.body.forEach (data) -> 139 | written += data 140 | 141 | q.when forEachFinished, -> 142 | expect(written).toBe msg 143 | done() 144 | 145 | ).fail (err) => 146 | this.fail err 147 | done() 148 | 149 | describe 'redirect given options', -> 150 | res = null 151 | opts = null 152 | 153 | beforeEach -> 154 | opts = { hello: 'world' } 155 | res = bogart.redirect '/', opts 156 | 157 | it 'should have correctly merged `hello` value', -> 158 | expect(res.hello).toBe opts.hello 159 | 160 | describe 'redirect given headers', -> 161 | res = null 162 | headers = null 163 | 164 | beforeEach -> 165 | headers = { hello: 'world' } 166 | 167 | res = bogart.redirect '/', { headers: headers } 168 | 169 | it 'should have correct location header', -> 170 | expect(res.headers.location).toBe '/' 171 | 172 | it 'should have correct `hello` header', -> 173 | expect(res.headers.hello).toBe headers.hello 174 | 175 | -------------------------------------------------------------------------------- /spec/routerSpec.coffee: -------------------------------------------------------------------------------- 1 | bogart = require '../lib/bogart' 2 | MockRequest = require './helpers/JsgiRequestHelper' 3 | Router = (require '../lib/router').Router 4 | q = require 'q' 5 | 6 | describe 'matches parameter', -> 7 | req = null 8 | router = null 9 | res = null 10 | name = null 11 | 12 | beforeEach -> 13 | req = MockRequest.root() 14 | 15 | router = bogart.router() 16 | 17 | router.get '/hello/:name', (req) -> 18 | name = req.params.name 19 | bogart.html 'hello' 20 | 21 | req.pathInfo = '/hello/nathan' 22 | 23 | res = router() req 24 | 25 | it 'should have correct status', (done) -> 26 | q.when res, (res) -> 27 | expect(res.status).toBe 200 28 | done() 29 | 30 | it 'should have correct name', (done) -> 31 | q.when res, (res) -> 32 | expect(name).toBe 'nathan' 33 | done() 34 | 35 | describe 'order of routes matching should be in order defined', -> 36 | router = null 37 | res = null 38 | firstCalled = false 39 | secondCalled = false 40 | 41 | beforeEach -> 42 | router = bogart.router() 43 | 44 | router.get '/hello/:name', (req) -> 45 | firstCalled = true 46 | bogart.html 'hello' 47 | 48 | router.get '/hello/:name/:something', (req) -> 49 | secondCalled = true 50 | bogart.html 'hello' 51 | 52 | res = router() new MockRequest('/hello/nathan') 53 | 54 | it 'should have called first route', (done) -> 55 | q.when res, (res) -> 56 | expect(firstCalled).toBe true 57 | done() 58 | 59 | it 'should not have called second route', (done) -> 60 | q.when res, -> 61 | expect(secondCalled).toBe false 62 | done() 63 | 64 | describe 'should call notFoundApp', -> 65 | notFoundApp = null 66 | router = null 67 | res = null 68 | called = false 69 | notFoundRes = null 70 | 71 | beforeEach -> 72 | notFoundRes = { status: 404, body: [ '' ], headers: {} } 73 | 74 | notFoundApp = (req) -> 75 | called = true 76 | notFoundRes 77 | 78 | router = bogart.router() 79 | 80 | res = router(notFoundApp) MockRequest.root() 81 | 82 | it 'should have correct response', (done) -> 83 | q.when res, (res) -> 84 | expect(res).toBe notFoundRes 85 | done() 86 | 87 | it 'should have called the not found app', (done) -> 88 | q.when res, (res) -> 89 | expect(called).toBe true 90 | done() 91 | 92 | 93 | describe 'default notFoundApp behavior of returning 404', -> 94 | res = null 95 | 96 | beforeEach -> 97 | router = bogart.router() 98 | 99 | res = router() MockRequest.root() 100 | 101 | it 'should have status of 404', (done) -> 102 | q.when res, (res) -> 103 | expect(res.status).toBe 404 104 | done() 105 | 106 | describe 'router.notFound', -> 107 | notFoundHandler = null 108 | req = null 109 | res = null 110 | 111 | beforeEach -> 112 | notFoundHandler = jasmine.createSpy 'not found handler' 113 | 114 | router = bogart.router() 115 | router.notFound(notFoundHandler) 116 | 117 | req = MockRequest.root() 118 | res = router() req 119 | 120 | it 'should have called handler specified by notFound', -> 121 | q.when res, -> 122 | expect(notFoundHandler).toHaveBeenCalled() 123 | 124 | describe 'partially matched route', -> 125 | res = null 126 | 127 | beforeEach -> 128 | router = bogart.router() 129 | 130 | router.get '/partial-match', (req) -> 131 | { status: 200, body: [ 'hello' ], headers: {} } 132 | 133 | res = router() new MockRequest('/partial-match/path') 134 | 135 | it 'should have status of 404', (done) -> 136 | q.when res, (res) -> 137 | expect(res.status).toBe 404 138 | done() 139 | 140 | describe 'partially matched route with parameter', -> 141 | res = null 142 | 143 | beforeEach -> 144 | router = bogart.router() 145 | router.get '/:foo', (req) -> 146 | return { status: 200, body: [ 'hello' ], headers: {} } 147 | 148 | res = router() new MockRequest('/hello/world') 149 | 150 | it 'should have status of 404', (done) -> 151 | q.when res, (res) -> 152 | expect(res.status).toBe 404 153 | done() 154 | 155 | describe 'route with querystring', -> 156 | res = null 157 | 158 | beforeEach -> 159 | router = bogart.router() 160 | 161 | router.get '/home', (req) -> 162 | { status: 200, body: [ 'home' ], headers: {} } 163 | 164 | req = new MockRequest('/home') 165 | req.queryString = 'hello=world' 166 | 167 | res = router() req 168 | 169 | it 'should have correct status', (done) -> 170 | q.when res, (res) -> 171 | expect(res.status).toBe 200 172 | done() 173 | 174 | describe 'regex route', -> 175 | res = null 176 | splat = null 177 | routeRes = null 178 | 179 | beforeEach -> 180 | router = bogart.router() 181 | 182 | routeRes = bogart.html 'hello' 183 | 184 | router.get /\/hello\/(.*)\/(.*)/, (req) -> 185 | splat = req.params.splat 186 | routeRes 187 | 188 | res = router() new MockRequest('/hello/cruel/world') 189 | 190 | it 'should have correct splat', (done) -> 191 | q.when res, (res) -> 192 | expect(splat).toEqual [ 'cruel', 'world' ] 193 | done() 194 | 195 | it 'should have correct response', (done) -> 196 | q.when res, (res) -> 197 | expect(res).toBe routeRes 198 | done() 199 | 200 | describe 'request path with encoded slashes', -> 201 | res = null 202 | routeRes = null 203 | 204 | beforeEach -> 205 | routeRes = bogart.html 'hello' 206 | 207 | router = bogart.router() 208 | 209 | router.get '/:foo', (req) -> 210 | routeRes 211 | 212 | res = router() new MockRequest('/foo%2fbar') 213 | 214 | it 'should have correct response', (done) -> 215 | q.when res, (res) -> 216 | expect(res).toBe routeRes 217 | done() 218 | 219 | describe 'request with a dot (".") as part of the named parameter', -> 220 | res = null 221 | params = null 222 | routeRes = null 223 | res = null 224 | 225 | beforeEach -> 226 | routeRes = bogart.html 'hello' 227 | 228 | router = bogart.router() 229 | router.get '/:foo/:bar', (req) -> 230 | params = req.params 231 | routeRes 232 | 233 | res = router() new MockRequest('/user@example.com/name') 234 | 235 | 236 | it 'should have correct :foo param', (done) -> 237 | q.when res, -> 238 | expect(params.foo).toBe 'user@example.com' 239 | done() 240 | 241 | it 'should have correct :bar param', (done) -> 242 | q.when res, -> 243 | expect(params.bar).toBe 'name' 244 | done() 245 | 246 | it 'should have correct response', (done) -> 247 | q.when res, (res) -> 248 | expect(res).toBe routeRes 249 | done() 250 | 251 | describe 'matches empty `pathInfo` to "/" if no route is defined for ""', -> 252 | res = null 253 | routeRes = null 254 | 255 | beforeEach -> 256 | routeRes = bogart.text 'success' 257 | 258 | router = bogart.router() 259 | router.get '/', -> 260 | routeRes 261 | 262 | res = router() new MockRequest('') 263 | 264 | it 'should have correct response', (done) -> 265 | q.when res, (res) -> 266 | expect(res).toBe routeRes 267 | done() 268 | 269 | describe 'matches empty `pathInfo` to "" if a route is defined for ""', -> 270 | res = null 271 | rightRouteRes = null 272 | wrongRouteRes = null 273 | 274 | beforeEach -> 275 | rightRouteRes = bogart.text 'right' 276 | wrongRouteRes = bogart.text 'wrong' 277 | 278 | router = bogart.router() 279 | 280 | router.get '', (req) -> 281 | rightRouteRes 282 | 283 | router.get '/', (req) -> 284 | wrongRouteRes 285 | 286 | res = router() new MockRequest('') 287 | 288 | it 'should have correct response', (done) -> 289 | q.when res, (res) -> 290 | expect(res).toBe rightRouteRes 291 | done() 292 | 293 | describe 'paths that include spaces', -> 294 | res = null 295 | routeRes = null 296 | 297 | beforeEach -> 298 | routeRes = bogart.text 'spaces are cool' 299 | router = bogart.router() 300 | 301 | router.get '/path with spaces', (req) -> 302 | routeRes 303 | 304 | res = router() new MockRequest('/path%20with%20spaces') 305 | 306 | it 'should have correct response', (done) -> 307 | q.when res, (res) -> 308 | expect(res).toBe routeRes 309 | done() 310 | 311 | describe 'literal (".") in path', -> 312 | res = null 313 | routeRes = null 314 | 315 | beforeEach -> 316 | routeRes = bogart.text 'hello' 317 | 318 | router = bogart.router() 319 | 320 | router.get '/foo.bar', -> 321 | routeRes 322 | 323 | res = router() new MockRequest('/foo.bar') 324 | 325 | it 'should have correct response', (done) -> 326 | q.when res, (res) -> 327 | expect(res).toBe routeRes 328 | done() 329 | 330 | describe '("-") in path', -> 331 | res = null 332 | routeRes = null 333 | 334 | beforeEach -> 335 | routeRes = bogart.text 'hello' 336 | 337 | router = bogart.router() 338 | 339 | router.get '/foo/:bar/dash-url', -> 340 | routeRes 341 | 342 | res = router() new MockRequest('/foo/baz/dash-url') 343 | 344 | it 'should have correct response', (done) -> 345 | q.when res, (res) -> 346 | expect(res).toBe routeRes 347 | done() 348 | 349 | describe 'path with splat ("*")', -> 350 | routeRes = null 351 | res = null 352 | params = null 353 | 354 | beforeEach -> 355 | routeRes = bogart.text 'splat' 356 | 357 | router = bogart.router() 358 | 359 | router.get '/foo/*', (req) -> 360 | params = req.params 361 | routeRes 362 | 363 | res = router() new MockRequest('/foo/hello/there') 364 | 365 | it 'should have correct splat', (done) -> 366 | q.when res, (res) -> 367 | expect(params.splat[0]).toBe 'hello/there' 368 | done() 369 | 370 | it 'should have correct response', (done) -> 371 | q.when res, (res) -> 372 | expect(res).toBe routeRes 373 | done() 374 | 375 | describe 'path with multiple splat parameters', -> 376 | routeRes = null 377 | res = null 378 | params = null 379 | 380 | beforeEach -> 381 | routeRes = bogart.text 'splat' 382 | 383 | router = bogart.router() 384 | 385 | router.get '/download/*/*', (req) -> 386 | params = req.params 387 | routeRes 388 | 389 | res = router() new MockRequest('/download/images/ninja-cat.jpg') 390 | 391 | it 'should have correct splat', (done) -> 392 | q.when res, (res) -> 393 | expect(params.splat).toEqual [ 'images', 'ninja-cat.jpg' ] 394 | done() 395 | 396 | it 'should have correct response', (done) -> 397 | q.when res, (res) -> 398 | expect(res).toBe routeRes 399 | done() 400 | 401 | describe 'mixing splat and named parameters', -> 402 | routeRes = null 403 | res = null 404 | params = null 405 | 406 | beforeEach -> 407 | routeRes = bogart.text 'mix' 408 | 409 | router = bogart.router() 410 | 411 | router.get '/:foo/*', (req) -> 412 | params = req.params 413 | routeRes 414 | 415 | res = router() new MockRequest('/foo/bar/baz') 416 | 417 | it 'should have correct response', (done) -> 418 | q.when res, (res) -> 419 | expect(res).toBe routeRes 420 | done() 421 | 422 | it 'should have correct named parameter', (done) -> 423 | q.when res, (res) -> 424 | expect(params.foo).toBe 'foo' 425 | done() 426 | 427 | it 'should have correct splat parameter', (done) -> 428 | q.when res, (res) -> 429 | expect(params.splat).toEqual [ 'bar/baz' ] 430 | done() 431 | 432 | describe 'chaining route handlers', -> 433 | routeRes = null 434 | res = null 435 | hello = null 436 | 437 | beforeEach -> 438 | routeRes = bogart.text 'hello' 439 | 440 | router = bogart.router() 441 | 442 | firstHandler = (req, next) -> 443 | req.hello = 'world' 444 | next req 445 | 446 | secondHandler = (req) -> 447 | hello = req.hello 448 | routeRes 449 | 450 | router.get '/', firstHandler, secondHandler 451 | 452 | res = router() MockRequest.root() 453 | 454 | it 'should have correct response', (done) -> 455 | q.when res, (res) -> 456 | expect(res).toBe routeRes 457 | done() 458 | 459 | it 'should have correct value set by first handler', (done) -> 460 | q.when res, -> 461 | expect(hello).toBe 'world' 462 | done() 463 | 464 | -------------------------------------------------------------------------------- /spec/streamSpec.coffee: -------------------------------------------------------------------------------- 1 | bogart = require '../lib/bogart' 2 | q = bogart.q 3 | fs = require 'fs' 4 | Readable = require('stream').Readable 5 | ForEachStream = require '../lib/forEachStream' 6 | zlib = require 'zlib' 7 | 8 | describe 'ForEachStream pipe to file stream', -> 9 | stat = null 10 | 11 | beforeEach -> 12 | stat = q.defer() 13 | 14 | source = [ 'Hello', 'World' ] 15 | stream = new ForEachStream source 16 | fileName = 'hello-world.txt' 17 | writeStream = fs.createWriteStream fileName 18 | 19 | stream.pipe writeStream 20 | 21 | stream.on 'end', -> 22 | stat.resolve(fs.statSync fileName) 23 | 24 | it 'should have created file', (done) -> 25 | q.when stat.promise, (stat) -> 26 | expect(stat.isFile()).toBe true 27 | done() 28 | 29 | describe 'ForEachStream pipe to deflate stream', -> 30 | stat = null 31 | bufferInflated = null 32 | 33 | beforeEach -> 34 | statDeferred = q.defer() 35 | bufferInflatedDeferred = q.defer() 36 | 37 | stat = statDeferred.promise 38 | bufferInflated = bufferInflatedDeferred.promise 39 | 40 | source = [ 'Hello', ' ', 'World' ] 41 | srcStream = new ForEachStream source 42 | stream = zlib.createDeflateRaw() 43 | fileName = 'hello-world.dat' 44 | 45 | srcStream.pipe(stream).pipe(fs.createWriteStream(fileName)) 46 | 47 | stream.on 'end', -> 48 | statDeferred.resolve fs.statSync(fileName) 49 | 50 | process.nextTick -> 51 | readStream = fs.createReadStream fileName 52 | inflateStream = zlib.createInflateRaw() 53 | buffer = new Buffer 0 54 | 55 | readStream.pipe inflateStream 56 | 57 | inflateStream.on 'data', (data) -> 58 | oldBuffer = buffer 59 | 60 | buffer = new Buffer(oldBuffer.length + data.length) 61 | oldBuffer.copy(buffer, 0, 0) 62 | data.copy(buffer, oldBuffer.length, 0) 63 | 64 | inflateStream.on 'error', (err) => 65 | this.fail err 66 | 67 | inflateStream.on 'end', -> 68 | bufferInflatedDeferred.resolve buffer.toString('utf-8') 69 | 70 | it 'should have correct inflated buffer', (done) -> 71 | q.when bufferInflated, (str) -> 72 | expect(str).toBe 'Hello World' 73 | done() 74 | 75 | it 'should have created file', (done) -> 76 | q.when stat, (stat) -> 77 | expect(stat.isFile()).toBe true 78 | done() 79 | 80 | describe 'pump ForEachStream to file stream', -> 81 | ended = null 82 | fileName = null 83 | 84 | beforeEach -> 85 | seed = [ 'Hello', ' ', 'World' ].map (x) -> 86 | new Buffer x 87 | 88 | fileName = 'forEachableToFileStream.txt' 89 | 90 | src = new ForEachStream seed 91 | 92 | dest = fs.createWriteStream fileName 93 | 94 | ended = bogart.pump src, dest 95 | 96 | it 'should create file', (done) -> 97 | q.when ended, -> 98 | expect(fs.statSync(fileName).isFile()).toBe true 99 | done() 100 | 101 | it 'should have correct file content', (done) -> 102 | q.when ended, -> 103 | expect(fs.readFileSync(fileName, 'utf-8')).toBe 'Hello World' 104 | done() 105 | -------------------------------------------------------------------------------- /spec/utilSpec.coffee: -------------------------------------------------------------------------------- 1 | bogart = require '../lib/bogart' 2 | path = require 'path' 3 | q = bogart.q 4 | security = require '../lib/security' 5 | 6 | describe 'security', -> 7 | dec = null 8 | plain = null 9 | 10 | beforeEach -> 11 | plain = "this is a very long string of text with random characters {}[[]\!@#$%^&*()_+!@#$%^&*()_+%3D%44%5Z,this is a very long string of text with random characters {}[[]\!@#$%^&*()_+!@#$%^&*()_+%3D%44%5Z,this is a very long string of text with random characters {}[[]\!@#$%^&*()_+!@#$%^&*()_+%3D%44%5Z,this is a very long string of text with random characters {}[[]\!@#$%^&*()_+!@#$%^&*()_+%3D%44%5Z"; 12 | 13 | describe 'default key', -> 14 | beforeEach -> 15 | enc = security.encrypt plain 16 | dec = security.decrypt enc 17 | 18 | afterEach -> 19 | dec = null 20 | 21 | it 'should have decrypted original message', -> 22 | expect(dec).toBe plain 23 | 24 | describe 'provided key', -> 25 | beforeEach -> 26 | key = "THIS is a Secret -- key == C!@#$%^&*())_%3D%2F" 27 | 28 | enc = security.encrypt plain, key 29 | dec = security.decrypt enc, key 30 | 31 | afterEach -> 32 | dec = null 33 | 34 | it 'should have decrypted original message', -> 35 | expect(dec).toBe plain 36 | 37 | describe 'uuid key', -> 38 | beforeEach -> 39 | key = "0ce05d12-1a33-11e1-a436-0019e34411d1" 40 | 41 | enc = security.encrypt plain, key 42 | dec = security.decrypt enc, key 43 | 44 | afterEach -> 45 | dec = null 46 | 47 | it 'should have decrypted original message', -> 48 | expect(dec).toBe plain 49 | 50 | describe 'promisify', -> 51 | 52 | describe 'given success', -> 53 | expectedVal = 1 54 | res = null 55 | 56 | beforeEach -> 57 | asyncFn = (cb) -> 58 | process.nextTick -> 59 | cb null, expectedVal 60 | 61 | res = bogart.promisify(asyncFn)() 62 | 63 | it 'should have corrected resolution', (done) -> 64 | q.when res, (res) -> 65 | expect(res).toBe expectedVal 66 | done() 67 | 68 | describe 'given rejection', -> 69 | res = null 70 | expectedRejection = new Error('bad stuff happened') 71 | 72 | beforeEach -> 73 | asyncFn = (cb) -> 74 | process.nextTick -> 75 | cb expectedRejection 76 | 77 | res = bogart.promisify(asyncFn)() 78 | 79 | it 'should have correct rejection', (done) -> 80 | q.when(res).fail (err) -> 81 | expect(err).toBe expectedRejection 82 | done() 83 | 84 | 85 | -------------------------------------------------------------------------------- /spec/viewSpec.coffee: -------------------------------------------------------------------------------- 1 | bogart = require '../lib/bogart' 2 | view = require '../lib/view' 3 | jsgi = require 'jsgi' 4 | q = bogart.q 5 | path = require 'path' 6 | fixturesPath = path.join __dirname, 'fixtures' 7 | 8 | describe 'render mustache', -> 9 | res = null 10 | 11 | beforeEach -> 12 | viewEngine = bogart.viewEngine 'mustache', fixturesPath 13 | 14 | res = viewEngine.render 'index.mustache', { layout: false } 15 | 16 | it 'should have correct html', (done) -> 17 | q.when res, (res) -> 18 | expect(res).toBe '

Hello World from Mustache

' 19 | done() 20 | 21 | describe 'render mustache with partials', -> 22 | res = null 23 | 24 | beforeEach -> 25 | viewEngine = bogart.viewEngine 'mustache', fixturesPath 26 | 27 | res = viewEngine.render 'partial-test.mustache', { 28 | layout: false, 29 | locals: { greeting: {} }, 30 | partials: { greeting: 'greeting.mustache' } 31 | } 32 | 33 | it 'should have correct html', (done) -> 34 | q.when res, (res) -> 35 | expect(res).toBe '

Hello World from Mustache

With Partial

' 36 | done() 37 | 38 | describe 'given headers to merge', -> 39 | res = null 40 | opts = null 41 | 42 | beforeEach -> 43 | viewEngine = bogart.viewEngine 'mustache', fixturesPath 44 | 45 | opts = { 46 | layout: false, 47 | headers: { abc: 123 } 48 | } 49 | 50 | res = viewEngine.respond 'index.mustache', opts 51 | 52 | it 'should have correct content-type', (done) -> 53 | q.when res, (res) -> 54 | expect(res.headers['content-type']).toBe 'text/html' 55 | done() 56 | 57 | it 'should have correct merged value', (done) -> 58 | q.when res, (res) -> 59 | expect(res.headers.abc).toBe opts.headers.abc 60 | done() 61 | --------------------------------------------------------------------------------