diff --git a/MIT-LICENSE.md b/MIT-LICENSE.md deleted file mode 100644 index 635fdff..0000000 --- a/MIT-LICENSE.md +++ /dev/null @@ -1,21 +0,0 @@ -The MIT License (MIT) - -Copyright (c) 2014 Sam Eubank - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md index 5ee5393..ae2d097 100644 --- a/README.md +++ b/README.md @@ -1,24 +1,25 @@ -# Za +# Za +[![NPM Version](https://img.shields.io/npm/v/za.svg)](https://npmjs.org/package/za) +[![Downloads](https://img.shields.io/npm/dm/za.svg)](https://npmjs.org/package/za) +[![Build Status](https://img.shields.io/travis/lighterio/za.svg)](https://travis-ci.org/lighterio/za) +[![Code Coverage](https://img.shields.io/coveralls/lighterio/za/master.svg)](https://coveralls.io/r/lighterio/za) +[![Dependencies](https://img.shields.io/david/lighterio/za.svg)](https://david-dm.org/lighterio/za) +[![Support](https://img.shields.io/gratipay/Lighter.io.svg)](https://gratipay.com/Lighter.io/) -[![NPM Version](https://badge.fury.io/js/za.png)](http://badge.fury.io/js/za) -[![Build Status](https://travis-ci.org/lighterio/za.png?branch=master)](https://travis-ci.org/lighterio/za) -[![Code Coverage](https://coveralls.io/repos/lighterio/za/badge.png?branch=master)](https://coveralls.io/r/lighterio/za) -[![Dependencies](https://david-dm.org/lighterio/za.png?theme=shields.io)](https://david-dm.org/lighterio/za) -[![Support](http://img.shields.io/gittip/zerious.png)](https://www.gittip.com/lighterio/) + +## TL;DR Za is a simple web server that exposes an Express-like API and is capable of handling far more requests per second than Express. -## Getting Started +### Quick Start Install `za` in your project: - ```bash npm install --save za ``` Use `za` to start an HTTP server: - ```javascript var server = require('./za')(); server.listen(8888); @@ -29,6 +30,7 @@ console.log('Visit http://localhost:8888/ for "Hello World!"'); ``` + ## Server and Router ### za() @@ -226,6 +228,96 @@ When the multipart parser is finished, the request emits a `"za:finished"` event. At that time, `request.multipart` (as well as `request.body`) contain all of the data that was sent in the multipart request. + ## Why is it called "Za"? + The term "za" is short for "pizza", and when it comes to web app responses and pizza, everyone wants fast delivery. (Also, the name was available on NPM). + + +## Acknowledgements + +We would like to thank all of the amazing people who use, support, +promote, enhance, document, patch, and submit comments & issues. +Za couldn't exist without you. + +Additionally, huge thanks go to [TUNE](http://www.tune.com) for employing +and supporting [Za](http://lighter.io/za) project maintainers, +and for being an epically awesome place to work (and play). + + +## MIT License + +Copyright (c) 2014 Sam Eubank + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +## How to Contribute + +We welcome contributions from the community and are happy to have them. +Please follow this guide when logging issues or making code changes. + +### Logging Issues + +All issues should be created using the +[new issue form](https://github.com/lighterio/za/issues/new). +Please describe the issue including steps to reproduce. Also, make sure +to indicate the version that has the issue. + +### Changing Code + +Code changes are welcome and encouraged! Please follow our process: + +1. Fork the repository on GitHub. +2. Fix the issue ensuring that your code follows the + [style guide](http://lighter.io/style-guide). +3. Add tests for your new code, ensuring that you have 100% code coverage. + (If necessary, we can help you reach 100% prior to merging.) + * Run `npm test` to run tests quickly, without testing coverage. + * Run `npm run cover` to test coverage and generate a report. + * Run `npm run report` to open the coverage report you generated. +4. [Pull requests](http://help.github.com/send-pull-requests/) should be made + to the [master branch](https://github.com/lighterio/za/tree/master). + +### Contributor Code of Conduct + +As contributors and maintainers of Za, we pledge to respect all +people who contribute through reporting issues, posting feature requests, +updating documentation, submitting pull requests or patches, and other +activities. + +If any participant in this project has issues or takes exception with a +contribution, they are obligated to provide constructive feedback and never +resort to personal attacks, trolling, public or private harassment, insults, or +other unprofessional conduct. + +Project maintainers have the right and responsibility to remove, edit, or +reject comments, commits, code, edits, issues, and other contributions +that are not aligned with this Code of Conduct. Project maintainers who do +not follow the Code of Conduct may be removed from the project team. + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported by opening an issue or contacting one or more of the project +maintainers. + +We promise to extend courtesy and respect to everyone involved in this project +regardless of gender, gender identity, sexual orientation, ability or +disability, ethnicity, religion, age, location, native language, or level of +experience. diff --git a/lib/Request.js b/lib/Request.js new file mode 100644 index 0000000..518f4e1 --- /dev/null +++ b/lib/Request.js @@ -0,0 +1,46 @@ +var http = require('http'); +var parse = require('./parse'); + +var proto = http.IncomingMessage.prototype; + +proto.header = function (name) { + return this.headers[(name || '').toLowerCase()]; +}; + +if (!proto.hasOwnProperty('cookies')) { + Object.defineProperty(proto, 'cookies', { + enumerable: false, + get: function () { + var cookies = {}; + var cookie = this.headers.cookie; + if (cookie) { + cookie.split(/; ?/).forEach(function (pair) { + pair = pair.split('='); + cookies[decodeURIComponent(pair[0])] = decodeURIComponent(pair[1]); + }); + } + this.cookies = cookies; + return cookies; + } + }); +} + +if (!proto.hasOwnProperty('query')) { + Object.defineProperty(proto, 'query', { + enumerable: false, + get: function () { + var query = parse(this.queryString); + this.query = query; + return query; + } + }); +} + +if (!proto.hasOwnProperty('body')) { + Object.defineProperty(proto, 'body', { + enumerable: false, + get: function () { + return this.multipart || parse(this.buffer); + } + }); +} diff --git a/lib/http.js b/lib/Response.js similarity index 50% rename from lib/http.js rename to lib/Response.js index 451c34c..4434d17 100644 --- a/lib/http.js +++ b/lib/Response.js @@ -1,21 +1,18 @@ var http = require('http'); var escape = require('querystring').escape; -var parse = require('./parse'); - // TODO: Benchmark zlib performance against node-compress. var zlib = require('zlib'); -var requestProto = http.IncomingMessage.prototype; -var responseProto = http.ServerResponse.prototype; +var proto = http.ServerResponse.prototype; -responseProto.json = function (object) { +proto.json = function (object) { var json = JSON.stringify(object); var res = this; res.setHeader('content-type', 'application/json'); res.send(json); }; -responseProto.send = function (data) { +proto.send = function (data) { var res = this; var type = typeof data; if (type == 'string') { @@ -33,7 +30,7 @@ responseProto.send = function (data) { } }; -responseProto.zip = function (text, preZipped) { +proto.zip = function (text, preZipped) { // TODO: Determine whether 1e3 is the right threshold. var res = this; if (preZipped || (text.length > 1e3)) { @@ -46,6 +43,7 @@ responseProto.zip = function (text, preZipped) { else { zlib.gzip(text, function (err, zipped) { if (err) { + console.log(err); res.end(text); } else { @@ -60,56 +58,15 @@ responseProto.zip = function (text, preZipped) { res.end(text); }; -responseProto.cookie = function (name, value, options) { +proto.cookie = function (name, value, options) { var res = this; res.setHeader('set-cookie', name + '=' + escape(value)); }; -responseProto.redirect = -responseProto.redirect || function (location) { +proto.redirect = +proto.redirect || function (location) { var res = this; res.statusCode = 302; res.setHeader('location', location); res.end(); }; - -requestProto.header = function (name) { - return this.headers[(name || '').toLowerCase()]; -}; - -if (!requestProto.hasOwnProperty('cookies')) { - Object.defineProperty(requestProto, 'cookies', { - get: function () { - var cookies = {}; - var cookie = this.headers.cookie; - if (cookie) { - cookie.split(/; ?/).forEach(function (pair) { - pair = pair.split('='); - cookies[decodeURIComponent(pair[0])] = decodeURIComponent(pair[1]); - }); - } - this.cookies = cookies; - return cookies; - } - }); -} - -if (!requestProto.hasOwnProperty('query')) { - Object.defineProperty(requestProto, 'query', { - enumerable: false, - get: function () { - var query = parse(this.queryString); - this.query = query; - return query; - } - }); -} - -if (!requestProto.hasOwnProperty('body')) { - Object.defineProperty(requestProto, 'body', { - enumerable: false, - get: function () { - return this.multipart || parse(this.buffer); - } - }); -} diff --git a/lib/Router.js b/lib/Router.js index e92119f..557ef6d 100644 --- a/lib/Router.js +++ b/lib/Router.js @@ -1,9 +1,24 @@ var multipart = require('./multipart'); +var doNothing = function () {}; -var paths; -var patterns; +// Maximum number of bytes we can receive (to avoid storage attacks). +var MAX_BYTES = 1e8; // ~100MB. -var MAX_BYTES = 1e8; // ~100MB +/** + * Turn a string into a RegExp pattern if it has asterisks. + */ +function patternify(str, start, end) { + if (typeof str == 'string') { + str = str.toLowerCase(); + if (str.indexOf('*') > -1) { + str = str.replace(/\*/g, '@'); + str = str.replace(/([^\d\w_-])/gi, '\\$1'); + str = str.replace(/\\@/g, '.*'); + return new RegExp(start + str + end, 'i'); + } + } + return str; +} /** * A Lighter Router sets up HTTP routing. @@ -13,20 +28,8 @@ module.exports = function Router(protocol) { var app; - var paths = this.paths = { - GET: {}, - PUT: {}, - POST: {}, - DELETE: {} - }; - - // TODO: Support routing patterns with wildcards. - var patterns = this.patterns = { - GET: {}, - PUT: {}, - POST: {}, - DELETE: {} - }; + // Paths are keys/items on/in the routes array. + var routes = this.routes = {}; // Middlewares are functions that chain asynchronously. var middlewares = this.middlewares = []; @@ -43,36 +46,52 @@ module.exports = function Router(protocol) { this.server = server; }; + /** + * Add a function to handle a specific HTTP method and URL path. + */ this.add = function add(method, path, fn) { - path = path.toLowerCase(); - fn._ZA_MAX_BYTES = fn._ZA_MAX_BYTES || MAX_BYTES; - if (path.indexOf('*') > -1) { - throw "Za routing does not yet support wildcards in paths. Cannot route " + path + "."; - } - else { - paths[method][path] = fn; + method = method.toUpperCase(); + path = patternify(path, '^', '$'); + var map = routes[method] = routes[method] || []; + + // Each route ultimately maps to a function. + map[path] = fn; + + // In addition to being a map, the map is an array of RegExp patterns. + if (path instanceof RegExp) { + map.push(path); } }; + /** + * Add a middleware, with an optional path. + */ this.use = function use(path, fn) { var middleware; if (typeof path == 'function') { middleware = path; } else { - var length = path.length; - middleware = function (request, response, next) { - if (request.url.substr(0, length) == path) { - fn.call(app, request, response, next); - } - else { - next(); - } - }; + path = patternify(path, '^', ''); + if (path instanceof RegExp) { + middleware = function (request) { + var isMatch = path.test(request.url); + return isMatch ? fn.apply(app, arguments) : true; + }; + } + else { + middleware = function (request) { + var isMatch = (request.url.indexOf(path) === 0); + return isMatch ? fn.apply(app, arguments) : true; + }; + } } middlewares.push(middleware); }; + /** + * Process a request and write to the response. + */ this.serve = function (request, response) { response.app = app; response.request = request; @@ -86,33 +105,64 @@ module.exports = function Router(protocol) { url = url.toLowerCase(); var m = middlewares; - var n = m.length; - var i = -1; + var i = 0; + /** + * Iterate over middlewares that return truthy values. + * Wait for falsy-returning middlewares to call `next`. + */ function next() { - if (++i < n) { - m[i].call(app, request, response, next); - } - else { - finish(); - } + var ok, fn; + do { + fn = m[i++]; + if (fn) { + ok = fn.call(app, request, response, next); + } + else { + return finish(); + } + } while (ok); } + /** + * Finish the request. + */ function finish() { - var fn = paths[request.method][url]; + var method = request.method; + + // TODO: Add automagic support for CONNECT/OPTIONS/TRACE? + if (method == 'HEAD') { + method = 'GET'; + response.write = doNothing; + } + var map = routes[method] || routes.GET; + var fn = map[url]; + + // If the path didn't map to a route, iterate over wildcard routes. + if (!fn) { + for (var i = 0, l = map.length; i < l; i++) { + var p = map[i]; + if (p.test(url)) { + fn = map[p]; + break; + } + } + } + if (fn) { - if (request.method[0] == 'P') { + if (method[0] == 'P') { + var maxBytes = fn._MAX_BYTES || MAX_BYTES; if (/multipart/.test(request.headers['content-type'])) { request.multipart = {}; fn(request, response); - multipart(request, response, fn._ZA_MAX_BYTES); + multipart(request, response, maxBytes); } else { var buffer; buffer = ''; request.on('data', function (data) { buffer += data; - if (buffer.length > fn._ZA_MAX_BYTES) { + if (buffer.length > maxBytes) { request.connection.destroy(); } }); @@ -126,8 +176,8 @@ module.exports = function Router(protocol) { fn(request, response); } } - else if (response.error404) { - response.error404(); + else if (response.error) { + response.error(404); } else { response.statusCode = 404; @@ -138,4 +188,4 @@ module.exports = function Router(protocol) { next(); }; -}; +}; \ No newline at end of file diff --git a/lib/Server.js b/lib/Server.js index 8af5066..43a8c12 100644 --- a/lib/Server.js +++ b/lib/Server.js @@ -1,134 +1,161 @@ var Router = require('./Router'); -var protocols = ['http', 'https']; var http = require('http'); var https = require('https'); +var protocols = ['http', 'https']; +var defaultPort = 8080; /** - * The Za server is an http/https web server. + * A Za Server is a fast, simple HTTP + HTTPS server. */ -module.exports = function Server() { +var Server = module.exports = function (config) { + var self = this; - // Za can stand alone if it needs to. - this.app = this; - - // Or it can be part of a Lighter app. - this.setApp = function setApp(app) { - this.app = app; - for (var key in this.routers) { - this.routers[key].setApp(app); - } - }; + // Za can have a parent `app`, but it defaults to being its own app. + self.app = this; - this.routers = { - http: new Router('http'), - https: new Router('https') - }; + // The server does not take requests until the `listen` method is called. + self.isListening = false; - this._addRoute = function _addRoute(method, path, fn, protocol) { - var self = this; - var isRouted = false; - protocols.forEach(function (p) { - if (!protocol || (protocol == p)) { - if (self.routers[p]) { - isRouted = true; - self.routers[p].add(method, path, fn); + // The `routers` method returns an array, optionally filtered by protocol. + var routers = self.routers = function (protocol) { + if (self.isListening) { + var list = []; + protocols.forEach(function (p) { + if ((!protocol || (protocol == p)) && routers[p]) { + list.push(routers[p]); } - } - }); - if (!isRouted) { - throw "Could not route " + method + " to " + path + " over " + (protocol || protocols.join(' or ')) + "."; + }); + return list; + } + else { + return protocol ? [routers[protocol]] : routers.list; } }; - this.get = function get(path, fn, protocol) { - this._addRoute('GET', path, fn, protocol); - return this; - }; + // The `routers` property is also a map. + routers.http = new Router('http'); + routers.https = new Router('https'); + routers.list = [routers.http, routers.https]; + + // If configuration is passed in, start listening. + if (config) { + self.listen(config); + } - this.put = function get(path, fn, protocol) { - this._addRoute('PUT', path, fn, protocol); +}; + +/** + * Za servers share chainable methods. + */ +var proto = Server.prototype = { + + /** + * Inject a parent app (such as Lighter MVC). + */ + setApp: function (app) { + this.app = app; + this.routers().forEach(function (router) { + router.setApp(app); + }); return this; - }; + }, - this.post = function get(path, fn, protocol) { - this._addRoute('POST', path, fn, protocol); + /** + * Set an HTTP method to route a path to a function. + */ + route: function (method, path, fn, protocol) { + var routers = this.routers(protocol); + this.routers(protocol).forEach(function (router) { + router.add(method, path, fn); + }); return this; - }; + }, - this.delete = function get(path, fn, protocol) { - this._addRoute('DELETE', path, fn, protocol); + /** + * Set a middleware function to be used, optionally on a given path. + */ + use: function (path, fn, protocol) { + this.routers(protocol).forEach(function (router) { + router.use(path, fn); + }); return this; - }; + }, - this.use = function use(path, middleware, protocol) { + /** + * Listen for HTTP/HTTPS requests based on a `config` object. + * For example: + * ```json + * { + * http: 80, + * https: 443, // Optional. + * key: '/private/key.pem', // Optional unless `https` is set. + * cert: 'private/cert.pem' // Optional unless `https` is set. + * } + * ``` + */ + listen: function (config) { var self = this; - var isUsing = false; - protocols.forEach(function (p) { - if (!protocol || (protocol == p)) { - if (self.routers[p]) { - self.routers[p].use(path, middleware); - isUsing = true; - } + var args = arguments; + config = config || 0; + var ssl = { + key: config.key ? fs.readFileSync(config.key) : 0, + cert: config.cert ? fs.readFileSync(config.cert) : 0 + }; + var routers = self.routers; + routers.list = []; + protocols.forEach(function (protocol, i) { + // Support `listen(httpPort, httpsPort, httpsOptions)` for backward compatibility. + var port = config[protocol] || args[i] || (i ? 0 : defaultPort++); + if (port) { + var router = routers[protocol] || new Router(protocol); + var lib = require(protocol); + var server = i ? + lib.createServer(ssl, router.serve) : + lib.createServer(router.serve); + routers[protocol] = router; + router.setServer(server); + server.listen(port); + self.isListening = true; + routers.list.push(router); + } + else { + delete routers[protocol]; } }); - if (!isUsing) { - throw "Could not use middleware over " + (protocol || protocols.join(' or ')) + "."; - } - }; - - this.listen = function listen(httpPort, httpsPort, httpsOptions) { - var self = this; - var router, server; - if (httpPort) { - router = self.routers.http; - router.setPort(httpPort); - server = http.createServer(router.serve); - router.setServer(server); - server.listen(httpPort); - } - else { - delete self.routers.http; - } - if (httpsPort) { - router = self.routers.https; - router.setPort(httpsPort); - server = https.createServer(httpsOptions, router.serve); - router.setServer(server); - server.listen(httpsPort); - } - else { - delete self.routers.https; - } return self; - }; + }, - this.close = function close() { + /** + * Shut the server down by stopping its routers' servers. + */ + close: function () { var self = this; - protocols.forEach(function (protocol) { - var router = self.routers[protocol]; - if (router) { + if (self.isListening) { + self.routers().forEach(function (router) { router.server.close(); - } - }); + }); + } + self.isListening = false; return self; - }; + }, /** - * returns requestListener compatible interface -> function (req, res) - * which can be used to create server using node http or https require - * - * It is useful to have an easy interface to get request listener without knowing za's inside - * It is also useful to setup a testing environment using supertest - * @see https://github.com/visionmedia/supertest + * Get a requestListener-compatible interface. + * @see https://github.com/visionmedia/supertest * @see http://nodejs.org/api/http.html#http_http_createserver_requestlistener - * - * @param {[type]} protocol za server contains routers for multiple protocol. specify protocol to choose specific za router to be returned as requestListener - * @return {[requestListener]} interface function (request, response) */ - this.requestListener = function requestListener(protocol) { - protocol = protocol || 'http'; // default protocol - var router = this.routers[protocol]; - return router.serve.bind(this); - }; + requestListener: function (protocol) { + var self = this; + return self.routers(protocol)[0].serve.bind(self); + } }; + +// Create HTTP method properties so that (e.g.) `app.get('/', fn)` can be called. +['CONNECT', 'DELETE', 'GET', 'HEAD', + 'OPTIONS', 'POST', 'PUT', 'TRACE'].forEach(function (method) { + var key = method.toLowerCase(); + proto[key] = function (path, fn, protocol) { + return this.route(method, path, fn, protocol); + }; +}); \ No newline at end of file diff --git a/package.json b/package.json index e38de64..00b4273 100644 --- a/package.json +++ b/package.json @@ -1,49 +1,35 @@ { "name": "za", + "version": "0.1.0", "description": "A fast Node.js web server", + "dependencies": {}, + "devDependencies": { + "coveralls": "2.10.0", + "istanbul": "0.2.11", + "qs": "0.6.6", + "supertest": "0.13.0", + "zeriousify": "^0.1.10", + "exam": "^0.1.2" + }, "keywords": [ "za" ], - "version": "0.0.21", "main": "za.js", - "homepage": "http://github.com/lighterio/za", - "repository": "http://github.com/lighterio/za.git", - "bugs": { - "url": "http://github.com/lighterio/za/issues" - }, - "author": { - "name": "Sam Eubank", - "email": "sameubank@gmail.com" + "engines": { + "node": ">=0.2.6" }, - "contributors": [ - { - "name": "Sam Eubank", - "email": "sameubank@gmail.com" - } - ], - "licenses": [ - { - "type": "MIT", - "url": "http://github.com/lighterio/za/blob/master/MIT-LICENSE.md" - } - ], - "engines": [ - "node >= 0.2.6" - ], "scripts": { "test": "./node_modules/exam/exam.js", - "retest": "./node_modules/exam/exam.js --watch", "cover": "istanbul cover ./node_modules/exam/exam.js", "report": "open coverage/lcov-report/index.html", - "coveralls": "istanbul cover ./node_modules/exam/exam.js --report lcovonly -- -R spec && cat ./coverage/lcov.info | coveralls && rm -rf ./coverage" + "coveralls": "istanbul cover ./node_modules/exam/exam.js && cat ./coverage/lcov.info | coveralls && rm -rf ./coverage" }, - "dependencies": {}, - "devDependencies": { - "coveralls": "2.10.0", - "istanbul": "0.2.11", - "qs": "0.6.6", - "supertest": "0.13.0", - "zeriousify": "0.1.7", - "exam": "0.0.7" - } + "repository": "https://github.com/lighterio/za.git", + "bugs": "https://github.com/lighterio/za/issues", + "homepage": "http://github.com/lighterio/za", + "author": "Sam Eubank ", + "contributors": [ + "Sam Eubank " + ], + "license": "MIT" } diff --git a/test/.exam.js b/test/.exam.js new file mode 100644 index 0000000..9d3d012 --- /dev/null +++ b/test/.exam.js @@ -0,0 +1,3 @@ +module.exports = { + ignore: /^(fixtures)$/ +}; \ No newline at end of file diff --git a/test/.examignore b/test/.examignore deleted file mode 100644 index 116caa1..0000000 --- a/test/.examignore +++ /dev/null @@ -1 +0,0 @@ -fixtures diff --git a/test/fixtures/http-test-server.js b/test/fixtures/http-test-server.js index 10afc4d..8702b76 100644 --- a/test/fixtures/http-test-server.js +++ b/test/fixtures/http-test-server.js @@ -1,67 +1,66 @@ -'use strict'; -var za = require('../../za'); -var responses = require('./response-fixtures'); -var bodies = require('./body-fixtures'); -var codes = [200, 401, 500]; -var jsonBody = responses.shortJson; -var longJsonBody = responses.longJson; -var unzipped = responses.longString; -var shortString = responses.shortString; +var za = require('../../za'); +var responses = require('./response-fixtures'); +var bodies = require('./body-fixtures'); +var codes = [200, 401, 500]; +var jsonBody = responses.shortJson; +var longJsonBody = responses.longJson; +var unzipped = responses.longString; +var shortString = responses.shortString; var server = za(); while (unzipped.length < 2e3) { - unzipped += 'a'; + unzipped += 'a'; } var i = 0; while (i++ < 1e3) { - var key = 'key' + i; - longJsonBody[key] = 'value' + i; + var key = 'key' + i; + longJsonBody[key] = 'value' + i; } server.get('/zip', function (req, res) { - res.zip(unzipped); + res.zip(unzipped); }); server.get('/unzipped', function (req, res) { - res.zip(shortString); + res.zip(shortString); }); server.get('/json', function (req, res) { - res.json(jsonBody); + res.json(jsonBody); }); server.get('/jsonzip', function (req, res) { - res.json(longJsonBody); + res.json(longJsonBody); }); server.get('/send/400', function (req, res) { - res.send(400); + res.send(400); }); // making send end with all status code codes.forEach(function (code) { - server.get('/send/' + code, function (req, res) { - res.send(parseInt(code)); - }); + server.get('/send/' + code, function (req, res) { + res.send(parseInt(code)); + }); }); server.get('/send/json', function (req, res) { - res.send(jsonBody); + res.send(jsonBody); }); server.get('/header', function (req, res) { - res.send(req.header('content-type')); + res.send(req.header('content-type')); }); server.get('/cookie', function (req, res) { - res.cookie('coo', 'kie'); - res.send(200); + res.cookie('coo', 'kie'); + res.send(200); }); server.get('/redirect', function (req, res) { - res.redirect('http://google.com'); + res.redirect('http://google.com'); }); module.exports = server; diff --git a/test/fixtures/router-test-server.js b/test/fixtures/router-test-server.js index d4550ec..88a21fc 100644 --- a/test/fixtures/router-test-server.js +++ b/test/fixtures/router-test-server.js @@ -8,6 +8,15 @@ server.get('/', function (req, res) { res.json(responses.shortJson); }); +server.get('/wild/*', function (req, res) { + var wild = req.url.replace(/^\/wild\//, ''); + res.json({wild: wild}); +}); + +server.get('/*/profile', function (req, res) { + res.json({profile: req.url.split('/')[1]}); +}); + server.post('/json', function (req, res) { res.json(req.body); }); @@ -30,6 +39,30 @@ server.use('/middleware', function (req, res, next) { next(); }); + +server.use(/sync/, function (req, res) { + req.middle = [1]; + return true; +}); + +server.use(/async/, function (req, res, next) { + req.middle.push(2); + next(); +}); + +server.use('/sync', function (req, res) { + return req.middle.push(3); +}); + +server.use('/sync/*/ok', function (req, res) { + return req.middle.push(4); +}); + +server.get(/sync/, function (req, res) { + res.json(req.middle); +}); + + server.get('/query', function (req, res) { var q = req.query; q.hello = q.hello; diff --git a/test/httpTest.js b/test/httpTest.js index b5f617a..9a3fd29 100644 --- a/test/httpTest.js +++ b/test/httpTest.js @@ -7,6 +7,13 @@ var codes = server.codes; describe('http', function () { + it('supports HEAD', function (done) { + request(server.requestListener()).head('/send/json') + .expect(200) + .expect('') + .end(done); + }); + describe('zip', function () { it('returns zipped content', function (done) { request(server.requestListener()).get('/zip') diff --git a/test/routerTest.js b/test/routerTest.js index 212594f..67e1c67 100644 --- a/test/routerTest.js +++ b/test/routerTest.js @@ -4,11 +4,41 @@ var server = require('./fixtures/router-test-server'); var bodies = require('./fixtures/body-fixtures'); var responses = require('./fixtures/response-fixtures'); -describe('Server', function () { - it('should execute middleware', function (done) { - request(server.requestListener()).get('/middleware') - .expect(200) - .expect(JSON.stringify([1,2]), done); +describe('Router', function () { + + describe('middlewares', function () { + it('execute in order', function (done) { + request(server.requestListener()) + .get('/middleware') + .send() + .expect(200) + .expect(JSON.stringify([1,2])) + .end(done); + }); + it('support async mode', function (done) { + request(server.requestListener()) + .get('/async') + .send() + .expect(200) + .expect(JSON.stringify([1,2])) + .end(done); + }); + it('support sync mode', function (done) { + request(server.requestListener()) + .get('/sync') + .send() + .expect(200) + .expect(JSON.stringify([1,3])) + .end(done); + }); + it('support wildcards', function (done) { + request(server.requestListener()) + .get('/sync/me/ok') + .send() + .expect(200) + .expect(JSON.stringify([1,3,4])) + .end(done); + }); }); describe('@requestListener', function () { @@ -19,13 +49,15 @@ describe('Server', function () { }); }); - describe('router', function () { - it('should return 404 response', function (done) { + describe('.serve', function () { + + it('should return a 404 if the route is not found', function (done) { request(server.requestListener()).get('/notfound') .expect(404) .end(done); }); - it('should parse query param', function (done) { + + it('should parse query params', function (done) { var queryObj = { hello: '12', world: 'too', @@ -36,6 +68,7 @@ describe('Server', function () { .expect(JSON.stringify(queryObj)) .end(done); }); + }); describe('body parser', function () { @@ -53,4 +86,22 @@ describe('Server', function () { .end(done); }); }); + + describe('wildcards', function () { + it('match anything', function (done) { + request(server.requestListener()) + .get('/wild/hello') + .send() + .expect('{"wild":"hello"}') + .end(done); + }); + it('can appear in the middle', function (done) { + request(server.requestListener()) + .get('/something/profile') + .send() + .expect('{"profile":"something"}') + .end(done); + }); + }); + }); diff --git a/za.js b/za.js index 5e8e11c..3b61306 100644 --- a/za.js +++ b/za.js @@ -1,12 +1,13 @@ -// Decorate http objects. -require('./lib/http'); +// Decorate Request and Response prototypes. +require('./lib/Request'); +require('./lib/Response'); // Allow us to instantiate HTTP and HTTPS servers. var Server = require('./lib/Server'); // The API is a function which returns a server. var za = module.exports = function () { - return new Server(); + return new Server(); }; // Expose the version number, but only load package JSON if a get is performed.