├── .gitignore ├── .travis.yml ├── .jshintrc ├── package.json ├── Gruntfile.js ├── LICENSE ├── lib └── map-router.js ├── README.md └── test └── map-router-test.js /.gitignore: -------------------------------------------------------------------------------- 1 | npm-debug.log 2 | node_modules 3 | .DS_Store -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 0.12 4 | - iojs 5 | before_install: 6 | - npm install -g grunt-cli 7 | notifications: 8 | email: 9 | - andris@kreata.ee -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "indent": 4, 3 | "node": true, 4 | "globalstrict": true, 5 | "evil": true, 6 | "unused": true, 7 | "undef": true, 8 | "newcap": true, 9 | "esnext": true, 10 | "curly": true, 11 | "eqeqeq": true, 12 | "expr": true, 13 | 14 | "predef": [ 15 | "describe", 16 | "it", 17 | "beforeEach", 18 | "afterEach" 19 | ] 20 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "map-router", 3 | "version": "1.0.0", 4 | "description": "Fast exact match router for Express.js", 5 | "main": "lib/map-router.js", 6 | "scripts": { 7 | "test": "grunt" 8 | }, 9 | "author": "Andris Reinman", 10 | "license": "MIT", 11 | "dependencies": {}, 12 | "devDependencies": { 13 | "chai": "^2.0.0", 14 | "grunt": "^0.4.5", 15 | "grunt-contrib-jshint": "^0.11.0", 16 | "grunt-mocha-test": "^0.12.7", 17 | "mocha": "^2.1.0", 18 | "smtp-connection": "^1.1.0" 19 | }, 20 | "keywords": [ 21 | "express", 22 | "router" 23 | ], 24 | "engines": { 25 | "node": ">=0.12" 26 | }, 27 | "repository": { 28 | "type": "git", 29 | "url": "git://github.com/andris9/map-router.git" 30 | } 31 | } -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = function(grunt) { 4 | 5 | // Project configuration. 6 | grunt.initConfig({ 7 | jshint: { 8 | all: ['lib/*.js', 'test/*.js', 'Gruntfile.js'], 9 | options: { 10 | jshintrc: '.jshintrc' 11 | } 12 | }, 13 | 14 | mochaTest: { 15 | all: { 16 | options: { 17 | reporter: 'spec' 18 | }, 19 | src: ['test/*-test.js'] 20 | } 21 | } 22 | }); 23 | 24 | // Load the plugin(s) 25 | grunt.loadNpmTasks('grunt-contrib-jshint'); 26 | grunt.loadNpmTasks('grunt-mocha-test'); 27 | 28 | // Tasks 29 | grunt.registerTask('default', ['jshint', 'mochaTest']); 30 | }; -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015 Andris Reinman 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 11 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 12 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 13 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 14 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 15 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 16 | SOFTWARE. 17 | -------------------------------------------------------------------------------- /lib/map-router.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports.MapRouter = MapRouter; 4 | 5 | /** 6 | * Creates a new router instance 7 | * 8 | * @constructor 9 | */ 10 | function MapRouter() { 11 | this.routes = {}; 12 | } 13 | 14 | /** 15 | * Registers a new route. The method 'ALL' is a special case – if a route is not found 16 | * for the selected method (eg. 'GET') then 'ALL is also checked. 17 | * 18 | * @param {String} method Method to use (eg 'GET') 19 | * @param {String} path Route path (eg. '/path/to/route') 20 | * @param {Function} handler Function to run when the route matches 21 | */ 22 | MapRouter.prototype.register = function(method, path, handler) { 23 | method = (method || 'GET').toUpperCase().trim(); 24 | 25 | if (!this.routes[method]) { 26 | this.routes[method] = new Map(); 27 | } 28 | 29 | this.routes[method].set(path, handler); 30 | }; 31 | 32 | /** 33 | * Removes a route from the routing table 34 | * 35 | * @param {String} method Method to use (eg 'GET') 36 | * @param {String} path Route path (eg. '/path/to/route') 37 | */ 38 | MapRouter.prototype.unregister = function(method, path) { 39 | method = (method || 'GET').toUpperCase().trim(); 40 | 41 | if (this.routes[method] && this.routes[method].has(path)) { 42 | this.routes[method].delete(path); 43 | } 44 | }; 45 | 46 | /** 47 | * Find a handler for a route. If it doesn't find a route for the selected method, then it 48 | * also tries 'ALL' 49 | * 50 | * @param {String} method Method to use (eg 'GET') 51 | * @param {String} path Route path (eg. '/path/to/route') 52 | * @returns {Function} Route handler or false if not found 53 | */ 54 | MapRouter.prototype.route = function(method, path) { 55 | method = (method || 'GET').toUpperCase().trim(); 56 | 57 | if (this.routes[method] && this.routes[method].has(path)) { 58 | return this.routes[method].get(path); 59 | } else if (method !== 'ALL' && this.routes.ALL && this.routes.ALL.has(path)) { 60 | return this.routes.ALL.get(path); 61 | } 62 | 63 | return false; 64 | }; 65 | 66 | /** 67 | * Returns express middleware 68 | * @returns {[type]} [description] 69 | */ 70 | MapRouter.prototype.middleware = function() { 71 | var selfie = this; 72 | 73 | return function(req, res, next) { 74 | 75 | var route = selfie.route(req.method, req.path); 76 | 77 | if (!route) { 78 | // nothing to do here 79 | return next(); 80 | } 81 | 82 | route(req, res); 83 | 84 | }; 85 | }; 86 | 87 | // Set convinience wrappers 88 | ['all', 'get', 'post', 'put', 'delete'].forEach(function(method) { 89 | MapRouter.prototype[method] = function(path, handler) { 90 | this.register(method.toUpperCase(), path, handler); 91 | }; 92 | }); -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MapRouter 2 | 3 | **MapRouter** is a simple and fast *exact-match* router middleware for Express that can be used instead of the built-in router. 4 | 5 | Requires Node v0.12 or iojs. The module does not run on Node v0.10 as it uses [Map](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map) constructor which is not enabled by default (you might get it working with the `--harmony` flag though). 6 | 7 | [![Build Status](https://secure.travis-ci.org/andris9/map-router.svg)](http://travis-ci.org/andris9/map-router) 8 | [![npm version](https://badge.fury.io/js/map-router.svg)](http://badge.fury.io/js/map-router) 9 | 10 | The goals of this router are: 11 | 12 | * Should be very fast 13 | * Routes can be added dynamically 14 | * Routes can be removed dynamically 15 | 16 | The sacrifices for achieving the goals are: 17 | 18 | * Only exact matches, no regular expressions or variable support 19 | 20 | This router is suitable for scenarios where you need to add and remove routes very often in runtime. 21 | 22 | ## Usage 23 | 24 | Install with npm 25 | 26 | npm install map-router 27 | 28 | Require in your script 29 | 30 | var MapRouter = require('map-router'); 31 | 32 | Create MapRouter instance 33 | 34 | ```javascript 35 | var router = new MapRouter(); 36 | ``` 37 | 38 | Attach the router to Express 39 | 40 | ```javascript 41 | app.use(router.middleware()); 42 | ``` 43 | 44 | ### Add a new Route 45 | 46 | ```javascript 47 | router.register(method, path, handler) 48 | ``` 49 | 50 | Where 51 | 52 | * **method** is the method to use or `'ALL'` for matching all methods 53 | * **path** is the path to the route (eg. `'/path/to/handler'`) 54 | * **handler** is the route handler 55 | 56 | **Example** 57 | 58 | ```javascript 59 | var express = require('express'); 60 | var app = express(); 61 | var MapRouter = require('map-router'); 62 | var router = new MapRouter(); 63 | 64 | app.use(router.middleware()); 65 | 66 | router.register('GET', '/', function(req, res){ 67 | res.send('hello world'); 68 | }); 69 | ``` 70 | 71 | Alternatively you can register handlers with shorthand wrappers where method is the function name: 72 | 73 | ```javascript 74 | router.get('/', function(req, res){ 75 | res.send('hello world'); 76 | }); 77 | ``` 78 | 79 | ### Remove a Route 80 | 81 | ```javascript 82 | router.unregister(method, path) 83 | ``` 84 | 85 | Where 86 | 87 | * **method** is the method to use 88 | * **path** is the path to the route (eg. `'/path/to/handler'`) 89 | 90 | ```javascript 91 | router.unregister('GET', '/'); 92 | ``` 93 | 94 | ### Find a Route for a Path 95 | 96 | If you are not using the Express compatible middleware but want to do your own routing, you can do this with 97 | 98 | ```javascript 99 | var handler = router.route(method, path) 100 | ``` 101 | 102 | Where 103 | 104 | * **method** is the method to use or `'ALL'` for matching all methods 105 | * **path** is the path to the route (eg. `'/path/to/handler'`) 106 | * **handler** is the resolved handler function or false if the route was not found 107 | 108 | ## License 109 | 110 | **MIT** 111 | -------------------------------------------------------------------------------- /test/map-router-test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var chai = require('chai'); 4 | var MapRouter = require('../lib/map-router').MapRouter; 5 | var expect = chai.expect; 6 | 7 | chai.config.includeStack = true; 8 | 9 | describe('MapRouter tests', function() { 10 | 11 | it('should register a route', function() { 12 | var router = new MapRouter(); 13 | expect(router.routes.GET).to.not.exist; 14 | router.register('get', '/testpath', function() {}); 15 | expect(router.routes.GET).to.exist; 16 | expect(router.routes.GET.has('/testpath')).to.be.true; 17 | }); 18 | 19 | it('should unregister a route', function() { 20 | var router = new MapRouter(); 21 | expect(router.routes.GET).to.not.exist; 22 | router.register('get', '/testpath', function() {}); 23 | expect(router.routes.GET).to.exist; 24 | router.unregister('get', '/testpath'); 25 | expect(router.routes.GET.has('/testpath')).to.be.false; 26 | }); 27 | 28 | it('should match default method', function() { 29 | var router = new MapRouter(); 30 | expect(router.routes.GET).to.not.exist; 31 | router.register('get', '/testpath', function() {}); 32 | expect(router.routes.GET).to.exist; 33 | expect(router.route('GET', '/testpath')).to.exist; 34 | }); 35 | 36 | it('should match ALL', function() { 37 | var router = new MapRouter(); 38 | expect(router.routes.ALL).to.not.exist; 39 | router.register('all', '/testpath', function() {}); 40 | expect(router.routes.ALL).to.exist; 41 | expect(router.route('GET', '/testpath')).to.exist; 42 | }); 43 | 44 | it('should not match missing method', function() { 45 | var router = new MapRouter(); 46 | expect(router.routes.GET).to.not.exist; 47 | router.register('get', '/testpath', function() {}); 48 | expect(router.routes.GET).to.exist; 49 | expect(router.route('POST', '/testpath')).to.be.false; 50 | }); 51 | 52 | it('should register a route with shorthand method', function() { 53 | var router = new MapRouter(); 54 | expect(router.routes.GET).to.not.exist; 55 | router.get('/testpath', function() {}); 56 | expect(router.routes.GET).to.exist; 57 | expect(router.routes.GET.has('/testpath')).to.be.true; 58 | }); 59 | 60 | it('should route with middleware', function(done) { 61 | var router = new MapRouter(); 62 | var middleware = router.middleware(); 63 | router.get('/testpath', function(req) { 64 | expect(req.path).to.equal('/testpath'); 65 | done(); 66 | }); 67 | 68 | middleware({ 69 | method: 'GET', 70 | path: '/testpath' 71 | }, {}, function() { 72 | // should not be called 73 | expect(false).to.be.true; 74 | }); 75 | }); 76 | 77 | it('should not route with middleware', function(done) { 78 | var router = new MapRouter(); 79 | var middleware = router.middleware(); 80 | router.get('/testpath', function() { 81 | // should not be called 82 | expect(false).to.be.true; 83 | }); 84 | 85 | middleware({ 86 | method: 'POST', 87 | path: '/testpath' 88 | }, {}, function() { 89 | done(); 90 | }); 91 | }); 92 | }); --------------------------------------------------------------------------------