├── .gitignore ├── .jscsrc ├── .jshintignore ├── .travis.yml ├── LICENSE ├── README.md ├── examples ├── example1.js └── example2.js ├── index.js ├── package.json └── test └── index.js /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled source # 2 | ################### 3 | *.com 4 | *.class 5 | *.dll 6 | *.exe 7 | *.a 8 | *.o 9 | *.so 10 | *.node 11 | 12 | # Node Waf Byproducts # 13 | ####################### 14 | .lock-wscript 15 | build/ 16 | autom4te.cache/ 17 | 18 | # Node Modules # 19 | ################ 20 | # Better to let npm install these from the package.json defintion 21 | # rather than maintain this manually 22 | node_modules/ 23 | 24 | # Packages # 25 | ############ 26 | # it's better to unpack these files and commit the raw source 27 | # git has its own built in compression methods 28 | *.7z 29 | *.dmg 30 | *.gz 31 | *.iso 32 | *.jar 33 | *.rar 34 | *.tar 35 | *.zip 36 | 37 | # Logs and databases # 38 | ###################### 39 | *.log 40 | dump.rdb 41 | *.tap 42 | *.xml 43 | 44 | # OS generated files # 45 | ###################### 46 | .DS_Store? 47 | .DS_Store 48 | ehthumbs.db 49 | Icon? 50 | Thumbs.db 51 | coverage 52 | 53 | # Text Editor Byproducts # 54 | ########################## 55 | *.sw? 56 | .idea/ 57 | .jshintrc 58 | 59 | # Python object code 60 | ########################## 61 | *.py[oc] 62 | 63 | # All translation files # 64 | ######################### 65 | static/translations-s3/ 66 | -------------------------------------------------------------------------------- /.jscsrc: -------------------------------------------------------------------------------- 1 | { 2 | "requireCurlyBraces": [ 3 | "if", 4 | "else", 5 | "for", 6 | "while", 7 | "do", 8 | "try", 9 | "catch" 10 | ], 11 | "requireSpaceAfterKeywords": [ 12 | "if", 13 | "else", 14 | "for", 15 | "while", 16 | "do", 17 | "switch", 18 | "case", 19 | "return", 20 | "try", 21 | "catch", 22 | "function", 23 | "typeof" 24 | ], 25 | "requireSpaceBeforeBlockStatements": true, 26 | "requireParenthesesAroundIIFE": true, 27 | "requireSpacesInConditionalExpression": true, 28 | "disallowSpacesInNamedFunctionExpression": { 29 | "beforeOpeningRoundBrace": true 30 | }, 31 | "disallowSpacesInFunctionDeclaration": { 32 | "beforeOpeningRoundBrace": true 33 | }, 34 | "requireBlocksOnNewline": 1, 35 | "disallowEmptyBlocks": true, 36 | "disallowSpacesInsideArrayBrackets": true, 37 | "disallowSpacesInsideParentheses": true, 38 | "disallowQuotedKeysInObjects": true, 39 | "disallowDanglingUnderscores": false, 40 | "disallowSpaceAfterObjectKeys": true, 41 | "requireCommaBeforeLineBreak": true, 42 | "disallowSpaceAfterPrefixUnaryOperators": true, 43 | "disallowSpaceBeforePostfixUnaryOperators": true, 44 | "disallowSpaceBeforeBinaryOperators": [ 45 | "," 46 | ], 47 | "requireSpaceBeforeBinaryOperators": true, 48 | "requireSpaceAfterBinaryOperators": true, 49 | "requireCamelCaseOrUpperCaseIdentifiers": true, 50 | "disallowKeywords": [ "with" ], 51 | "validateQuoteMarks": "'", 52 | "validateIndentation": 4, 53 | "disallowMixedSpacesAndTabs": true, 54 | "disallowTrailingWhitespace": true, 55 | "disallowTrailingComma": true, 56 | "disallowKeywordsOnNewLine": [ "else" ], 57 | "requireLineFeedAtFileEnd": true, 58 | "requireCapitalizedConstructors": true, 59 | "requireDotNotation": true, 60 | "disallowYodaConditions": true, 61 | "disallowNewlineBeforeBlockStatements": true, 62 | "maximumLineLength": 80 63 | } 64 | -------------------------------------------------------------------------------- /.jshintignore: -------------------------------------------------------------------------------- 1 | .gitignore -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "0.8" 4 | - "0.10" 5 | - "0.11" 6 | before_install: npm i npm@latest -g 7 | script: npm run travis 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014 Matt Esch. 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 above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # http-hash 2 | 3 | [![build status][build-png]][build] 4 | [![Coverage Status][cover-png]][cover] 5 | [![Davis Dependency status][dep-png]][dep] 6 | 7 | 8 | [![NPM][npm-png]][npm] 9 | 10 | 11 | 12 | HTTP router based on a strict path tree structure 13 | 14 | ## Example 1 : Basic routes 15 | 16 | ```js 17 | var HttpHash = require('http-hash'); 18 | 19 | // Create a new http hash 20 | var hash = HttpHash(); 21 | 22 | // Create a route mapping to /test/ 23 | hash.set('/test/:foo/', function (req, res) { 24 | res.end(); 25 | }); 26 | 27 | // Get a valid route 28 | var route = hash.get('/test/var'); 29 | console.log(route); 30 | /* 31 | -> { 32 | handler: function (req, res) {}, 33 | params: { 34 | foo: 'var' 35 | }, 36 | splat: null 37 | } 38 | */ 39 | 40 | // Get an invalid route (returns null) 41 | var missing = hash.get('/missing'); 42 | console.log(missing); 43 | /* 44 | -> { 45 | handler: null, 46 | params: {}, 47 | splat: null 48 | } 49 | */ 50 | 51 | ``` 52 | 53 | ## Example 2 : Trailing splats 54 | 55 | ```js 56 | var HttpHash = require('http-hash'); 57 | 58 | // Create a new http hash 59 | var hash = HttpHash(); 60 | 61 | // Create a route mapping to /foo// 62 | hash.set('/foo/:test/*', function (req, res) { 63 | res.end(); 64 | }); 65 | 66 | var route = hash.get('/foo/val/one/two/three'); 67 | console.log(route); 68 | /* 69 | -> { 70 | handler: function (req, res) { ... }, 71 | params: { 72 | test: 'val' 73 | }, 74 | splat: 'one/two/three' 75 | } 76 | */ 77 | ``` 78 | 79 | ## Overview 80 | 81 | The most popular node routers are based on regular expression 82 | matching. This means that the order in which the routes are 83 | defined affects the resolution of a route to handler. Sometimes 84 | this is desirable, but it would often be better to have a 85 | resolution scheme that is easier to reason about. 86 | 87 | `http-hash` solves the routing problem by making route resolution 88 | independent of the order in which routes are defined. It does so 89 | by breaking a path into a tree of nodes, based on a simple split 90 | on `/`. For example, the route `/foo/bar/baz` is treated as tree 91 | nodes `foo > bar > baz`. We call `foo`, `bar` and `baz` path 92 | segments. 93 | 94 | Theses path segments are arranged into a tree of nodes, where 95 | each segment defines a node in the tree. Each node can point to: 96 | 97 | - a fixed handler `node.handler`, otherwise known as the node 98 | value 99 | 100 | - a set of static paths indexed by path name `node.staticPaths` 101 | 102 | - a variable subtree `node.variablePaths`, that can match a 103 | single named parameter OR the remainder of a route (splat). 104 | 105 | 106 | If the last character of a defined route is `*`, a variable path 107 | (or splat) will be inserted, consuming the rest of the path. 108 | This allows for subrouting, i.e. if you want to mount a static 109 | filesystem on `/fs` you would set the path as `/fs/*` where 110 | the nodes are broken down into the tree `fs > *`. The remainder 111 | of the route will be returned as a "splat" value, allowing for 112 | further routing. 113 | 114 | In the simple case, the route tree is based on exact matches on 115 | the name of the segment. That is to say, for the case where we 116 | want to match `/foo/bar/baz`, the tree looks like 117 | 118 | ```js 119 | { 120 | staticPaths: { 121 | foo: { 122 | staticPaths: { 123 | bar: { 124 | staticPaths: { 125 | baz: { 126 | handler: function (req, res) {} 127 | } 128 | } 129 | } 130 | } 131 | } 132 | } 133 | } 134 | ``` 135 | 136 | When defining routes, variable paths may be specified. This is 137 | where path segments are prefixed with `:` i.e. `/foo/:bar/baz`. 138 | 139 | For the `:bar` segment, the route consumes the single variable 140 | route slot for that node in the tree. So for example, the route 141 | `/foo/:bar/baz looks like 142 | 143 | ```js 144 | { 145 | staticPaths: { 146 | foo: { 147 | variablePaths: { 148 | staticPaths: { 149 | 'baz': function (req, res) {} 150 | } 151 | } 152 | } 153 | } 154 | } 155 | ``` 156 | 157 | Since a node can have both static and dynamic paths associated 158 | with it, the static path will win over the variable path when we 159 | resolve the path. 160 | 161 | ### Trailing slashes 162 | 163 | In most cases the trailing / does not matter. Variables cannot be 164 | the empty string, and neither can splats. A splat value will not 165 | contain the leading slash as it is consumed by the parent node. 166 | 167 | ### Path conflicts 168 | 169 | If a path conflict occurs, an exception will be thrown. Conflicts 170 | occur when: 171 | 172 | - A route is defined twice, resolving to two handlers 173 | 174 | This is the simplest case where `/foo` has been defined twice. 175 | 176 | 177 | - Variable names in the path are different 178 | 179 | Note that `/foo/:vara/` and `/foo/:varb/` conflict, since they 180 | both resolve to `foo.variablePaths`, but have different param 181 | names. 182 | 183 | 184 | - A variable route is defined for a splat node 185 | 186 | In the case of splats being defined at a level, no other 187 | other variables may be specified, as we cannot distinguish 188 | between `/foo/:var` and `/foo/*`. It is however ok to put static 189 | paths on the same level, i.e. `/foo/bar` and `/foo/*`. In this 190 | case, the static paths will be tried first before yielding the 191 | splat. 192 | 193 | 194 | ## Docs 195 | 196 | ### `var hash = HttpHash()` 197 | 198 | ```ocaml 199 | http-hash := () => HttpHash 200 | 201 | type HttpHash := { 202 | get: (String: url) => RouteResult, 203 | set: (String: path, Any: handler) => void, 204 | _hash: RouteNode 205 | } 206 | 207 | type RouteNode := { 208 | handler: Any, 209 | fixedPaths: Object, 210 | variablePaths: RouteNode | null 211 | } 212 | 213 | type RouteResult := { 214 | handler: Any | null, 215 | params: Object, 216 | splat: String | null 217 | } 218 | ``` 219 | 220 | `http-hash` exports a safe constructor function that when called 221 | returns a new `HttpHash`. `get` and `set` methods are exposed for 222 | public consumption and the underlying data structure `_hash` is 223 | exposed for private inspection/internal use. 224 | 225 | ### `hash.set(path, handler)` 226 | 227 | ```ocaml 228 | hash.set := (String: path, Any: handler) => void 229 | ``` 230 | 231 | Puts a path in the route table. If the path conflicts with an 232 | existing path, an exception will be thrown. 233 | 234 | Routes containing a `*` that are not part of a `/*` prefix will 235 | also throw an exception. 236 | 237 | A path should look like `/` or `/foo` or `/:foo` or a union of 238 | theses things, or optionally end with `/*` 239 | 240 | - param names should not be repeated as they will conflict but 241 | there is no strong assertion for this. The last param name 242 | wins. 243 | 244 | - specifying a variable twice for a node will cause an exception 245 | 246 | - repeated and trailing '/' are ignored 247 | 248 | - paths are case sensitive 249 | 250 | - variables and splats are not matched by the empty string. 251 | 252 | 253 | ### `hash.get(path)` 254 | 255 | ```ocaml 256 | hash.get := (String: path) => RouteResult 257 | ``` 258 | 259 | Gets a route from the route table. If there is no viable route, 260 | the handler will be returned as `null` in the `RouteResult` 261 | object. 262 | 263 | The route result contains a `params hash`, containing a key for 264 | each named variable in the path. Additionally, if a splat route 265 | was defined, the `splat` property will contain the tail portion 266 | of the route matched. 267 | 268 | 269 | ## Installation 270 | 271 | `npm install http-hash` 272 | 273 | ## Tests 274 | 275 | `npm test` 276 | 277 | ## Contributors 278 | 279 | - Matt Esch 280 | 281 | ## MIT Licensed 282 | 283 | [build-png]: https://secure.travis-ci.org/Matt-Esch/http-hash.png 284 | [build]: https://travis-ci.org/Matt-Esch/http-hash 285 | [cover-png]: https://coveralls.io/repos/Matt-Esch/http-hash/badge.png?branch=master 286 | [cover]: https://coveralls.io/r/Matt-Esch/http-hash 287 | [dep-png]: https://david-dm.org/Matt-Esch/http-hash.png 288 | [dep]: https://david-dm.org/Matt-Esch/http-hash 289 | [test-png]: https://ci.testling.com/Matt-Esch/http-hash.png 290 | [test]: https://ci.testling.com/Matt-Esch/http-hash 291 | [npm-png]: https://nodei.co/npm/http-hash.png?stars&downloads 292 | [npm]: https://nodei.co/npm/http-hash 293 | -------------------------------------------------------------------------------- /examples/example1.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var HttpHash = require('../index.js'); 4 | 5 | // Create a new http hash 6 | var hash = HttpHash(); 7 | 8 | // Create a route mapping to /test/ 9 | hash.set('/test/:foo/', function (req, res) { 10 | res.end(); 11 | }); 12 | 13 | // Get a valid route 14 | var route = hash.get('/test/var'); 15 | console.log(route); 16 | /* 17 | -> { 18 | handler: function (req, res) { ... }, 19 | params: { 20 | foo: 'var' 21 | }, 22 | splat: null 23 | } 24 | */ 25 | 26 | // Get an invalid route (returns null) 27 | var missing = hash.get('/missing'); 28 | console.log(missing); 29 | /* 30 | -> { 31 | handler: null, 32 | params: {}, 33 | splat: null 34 | } 35 | */ 36 | -------------------------------------------------------------------------------- /examples/example2.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var HttpHash = require('../index'); 4 | 5 | // Create a new http hash 6 | var hash = HttpHash(); 7 | 8 | // Create a route mapping to /test// 9 | hash.set('/foo/:test/*', function (req, res) { 10 | res.end(); 11 | }); 12 | 13 | var route = hash.get('/foo/val/one/two/three'); 14 | console.log(route); 15 | /* 16 | -> { 17 | handler: function (req, res) { ... }, 18 | params: { 19 | test: 'val' 20 | }, 21 | splat: 'one/two/three' 22 | } 23 | */ 24 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = HttpHash; 4 | 5 | function HttpHash() { 6 | if (!(this instanceof HttpHash)) { 7 | return new HttpHash(); 8 | } 9 | 10 | this._hash = new RouteNode(); 11 | } 12 | 13 | HttpHash.prototype.get = get; 14 | HttpHash.prototype.set = set; 15 | 16 | function get(pathname) { 17 | var pathSegments = pathname.split('/'); 18 | 19 | var hash = this._hash; 20 | var splat = null; 21 | var params = {}; 22 | var variablePaths; 23 | 24 | for (var i = 0; i < pathSegments.length; i++) { 25 | var segment = pathSegments[i]; 26 | 27 | if (!segment && !hash.isSplat) { 28 | continue; 29 | } else if ( 30 | segment === '__proto__' && 31 | hash.hasOwnProperty('proto') 32 | ) { 33 | hash = hash.proto; 34 | } else if (hash.staticPaths.hasOwnProperty(segment)) { 35 | hash = hash.staticPaths[segment]; 36 | } else if ((variablePaths = hash.variablePaths)) { 37 | if (variablePaths.isSplat) { 38 | splat = pathSegments.slice(i).join('/'); 39 | hash = variablePaths; 40 | break; 41 | } else { 42 | params[variablePaths.segment] = segment; 43 | hash = variablePaths; 44 | } 45 | } else { 46 | hash = null; 47 | break; 48 | } 49 | } 50 | 51 | // Match the empty splat 52 | if (hash && 53 | hash.handler === null && 54 | hash.variablePaths && 55 | hash.variablePaths.isSplat 56 | ) { 57 | splat = ''; 58 | hash = hash.variablePaths; 59 | } 60 | 61 | return new RouteResult(hash, params, splat); 62 | } 63 | 64 | function set(pathname, handler) { 65 | var pathSegments = pathname.split('/'); 66 | var hash = this._hash; 67 | var lastIndex = pathSegments.length - 1; 68 | var splatIndex = pathname.indexOf('*'); 69 | var hasSplat = splatIndex >= 0; 70 | 71 | if (hasSplat && splatIndex !== pathname.length - 1) { 72 | throw SplatError(pathname); 73 | } 74 | 75 | for (var i = 0; i < pathSegments.length; i++) { 76 | var segment = pathSegments[i]; 77 | 78 | if (!segment) { 79 | continue; 80 | } 81 | 82 | if (hasSplat && i === lastIndex) { 83 | hash = ( 84 | hash.variablePaths || 85 | (hash.variablePaths = new RouteNode(hash, segment, true)) 86 | ); 87 | 88 | if (!hash.isSplat) { 89 | throw RouteConflictError(pathname, hash); 90 | } 91 | } else if (segment.indexOf(':') === 0) { 92 | segment = segment.slice(1); 93 | hash = ( 94 | hash.variablePaths || 95 | (hash.variablePaths = new RouteNode(hash, segment)) 96 | ); 97 | 98 | if (hash.segment !== segment || hash.isSplat) { 99 | throw RouteConflictError(pathname, hash); 100 | } 101 | } else if (segment === '__proto__') { 102 | hash = ( 103 | ( 104 | hash.hasOwnProperty('proto') && 105 | hash.proto 106 | ) || 107 | (hash.proto = new RouteNode(hash, segment)) 108 | ); 109 | } else { 110 | hash = ( 111 | ( 112 | hash.staticPaths.hasOwnProperty(segment) && 113 | hash.staticPaths[segment] 114 | ) || 115 | (hash.staticPaths[segment] = new RouteNode(hash, segment)) 116 | ); 117 | } 118 | } 119 | 120 | if (hash.handler === null) { 121 | hash.src = pathname; 122 | hash.handler = handler; 123 | } else { 124 | throwRouteConflictError(pathname, hash); 125 | } 126 | } 127 | 128 | function RouteNode(parent, segment, isSplat) { 129 | this.parent = parent || null; 130 | this.segment = segment || null; 131 | this.handler = null; 132 | this.staticPaths = {}; 133 | this.variablePaths = null; 134 | this.isSplat = !!isSplat; 135 | this.src = null; 136 | } 137 | 138 | function RouteResult(node, params, splat) { 139 | this.handler = node && node.handler || null; 140 | this.splat = splat; 141 | this.params = params; 142 | this.src = node && node.src || null; 143 | } 144 | 145 | function SplatError(pathname) { 146 | var err = new Error('The splat * must be the last segment of the path'); 147 | err.pathname = pathname; 148 | return err; 149 | } 150 | 151 | function RouteConflictError(pathname, hash) { 152 | var conflictPath = hash.isSplat ? '' : '/'; 153 | 154 | while (hash && hash.parent) { 155 | var prefix = ( 156 | !hash.isSplat && 157 | hash === hash.parent.variablePaths 158 | ) ? ':' : ''; 159 | 160 | conflictPath = '/' + prefix + hash.segment + conflictPath; 161 | 162 | hash = hash.parent; 163 | } 164 | 165 | var err = new Error('Route conflict'); 166 | err.attemptedPath = pathname; 167 | err.conflictPath = conflictPath; 168 | 169 | return err; 170 | } 171 | 172 | // Break this out to prevent deoptimization of path.set 173 | function throwRouteConflictError(pathname, hash) { 174 | throw RouteConflictError(pathname, hash); 175 | } 176 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "http-hash", 3 | "version": "2.0.1", 4 | "description": "HTTP router based on a strict path tree structure", 5 | "keywords": [ 6 | "router", 7 | "http", 8 | "path", 9 | "hash" 10 | ], 11 | "author": "Matt Esch ", 12 | "repository": "git://github.com/Matt-Esch/http-hash.git", 13 | "main": "index", 14 | "homepage": "https://github.com/Matt-Esch/http-hash", 15 | "bugs": { 16 | "url": "https://github.com/Matt-Esch/http-hash/issues", 17 | "email": "matt@mattesch.info" 18 | }, 19 | "contributors": [ 20 | { 21 | "name": "Matt Esch" 22 | } 23 | ], 24 | "dependencies": {}, 25 | "devDependencies": { 26 | "coveralls": "^2.10.0", 27 | "istanbul": "^0.2.7", 28 | "jshint": "^2.5.0", 29 | "opn": "^1.0.0", 30 | "pre-commit": "0.0.5", 31 | "run-browser": "^1.3.0", 32 | "tap-spec": "^0.1.8", 33 | "tape": "^2.12.3" 34 | }, 35 | "license": "MIT", 36 | "scripts": { 37 | "test": "npm run jshint -s && NODE_ENV=test node test/index.js | tap-spec", 38 | "unit-test": "NODE_ENV=test node test/index.js | tap-spec", 39 | "jshint-pre-commit": "jshint --verbose $(git diff --cached --name-only | grep '\\.js$')", 40 | "jshint": "jshint --verbose .", 41 | "cover": "istanbul cover --report html --print detail test/index.js", 42 | "view-cover": "istanbul report html && opn ./coverage/index.html", 43 | "travis": "npm run cover -s && istanbul report lcov && ((cat coverage/lcov.info | coveralls) || exit 0)", 44 | "phantom": "run-browser test/index.js -b", 45 | "browser": "run-browser test/index.js" 46 | }, 47 | "engine": { 48 | "node": ">= 0.8.x" 49 | }, 50 | "pre-commit": [ 51 | "jshint-pre-commit", 52 | "unit-test" 53 | ], 54 | "ngen-version": "4.0.3" 55 | } 56 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var test = require('tape'); 4 | 5 | var HttpHash = require('../index.js'); 6 | 7 | test('httpHash is a function', function (assert) { 8 | assert.equal(typeof HttpHash, 'function'); 9 | assert.end(); 10 | }); 11 | 12 | test('http hash inserts root', function (assert) { 13 | // Arrange 14 | function routeHandler() {} 15 | 16 | var hash = HttpHash(); 17 | 18 | // Act 19 | hash.set('/', routeHandler); 20 | 21 | // Assert 22 | assert.strictEqual(hash._hash.handler, routeHandler); 23 | assert.end(); 24 | }); 25 | 26 | test('http hash inserts fixed route', function (assert) { 27 | // Arrange 28 | function routeHandler() {} 29 | 30 | var hash = HttpHash(); 31 | 32 | // Act 33 | hash.set('/test', routeHandler); 34 | 35 | // Assert 36 | assert.strictEqual( 37 | hash._hash.staticPaths.test.handler, 38 | routeHandler 39 | ); 40 | assert.end(); 41 | }); 42 | 43 | test('http hash inserts variable route', function (assert) { 44 | // Arrange 45 | function routeHandler() {} 46 | 47 | var hash = HttpHash(); 48 | 49 | // Act 50 | hash.set('/:test', routeHandler); 51 | 52 | // Assert 53 | assert.strictEqual( 54 | hash._hash.variablePaths.handler, 55 | routeHandler 56 | ); 57 | assert.end(); 58 | }); 59 | 60 | test('http hash retrieves root', function (assert) { 61 | // Arrange 62 | function routeHandler() {} 63 | 64 | var hash = HttpHash(); 65 | hash.set('/', routeHandler); 66 | 67 | var expectedParams = {}; 68 | 69 | // Act 70 | var result = hash.get('/'); 71 | 72 | // Assert 73 | assert.strictEqual(result.handler, routeHandler); 74 | assert.strictEqual(result.src, '/'); 75 | assert.strictEqual(result.splat, null); 76 | assert.deepEqual(result.params, expectedParams); 77 | assert.end(); 78 | }); 79 | 80 | test('http hash retrieves fixed route', function (assert) { 81 | // Arrange 82 | function routeHandler() {} 83 | 84 | var hash = HttpHash(); 85 | hash.set('/test', routeHandler); 86 | 87 | var expectedParams = {}; 88 | 89 | // Act 90 | var result = hash.get('/test'); 91 | 92 | // Assert 93 | assert.strictEqual(result.handler, routeHandler); 94 | assert.strictEqual(result.src, '/test'); 95 | assert.strictEqual(result.splat, null); 96 | assert.deepEqual(result.params, expectedParams); 97 | assert.end(); 98 | 99 | }); 100 | 101 | test('http hash retrieves variable route', function (assert) { 102 | // Arrange 103 | function routeHandler() {} 104 | 105 | var hash = HttpHash(); 106 | hash.set('/:test', routeHandler); 107 | 108 | var expectedParams = { test: 'hello' }; 109 | 110 | // Act 111 | var result = hash.get('/hello'); 112 | 113 | // Assert 114 | assert.strictEqual(result.handler, routeHandler); 115 | assert.strictEqual(result.src, '/:test'); 116 | assert.strictEqual(result.splat, null); 117 | assert.deepEqual(result.params, expectedParams); 118 | assert.end(); 119 | }); 120 | 121 | 122 | test('http hash retrieves null root', function (assert) { 123 | // Arrange 124 | var hash = HttpHash(); 125 | 126 | // Act 127 | var rootResult = hash.get('/'); 128 | var staticResult = hash.get('/a'); 129 | 130 | // Assert 131 | assert.strictEqual(rootResult.handler, null); 132 | assert.strictEqual(staticResult.handler, null); 133 | assert.end(); 134 | }); 135 | 136 | test('http hash retrieves null static', function (assert) { 137 | // Arrange 138 | function routeHandler() {} 139 | 140 | var hash = HttpHash(); 141 | hash.set('/a/b/c', routeHandler); 142 | 143 | // Act 144 | var rootResult = hash.get('/a/b/'); 145 | var staticResult = hash.get('/a/b/foo'); 146 | 147 | // Assert 148 | assert.strictEqual(staticResult.handler, null); 149 | assert.strictEqual(rootResult.handler, null); 150 | assert.end(); 151 | }); 152 | 153 | test('http hash retrieves null variable', function (assert) { 154 | // Arrange 155 | function routeHandler() {} 156 | 157 | var hash = HttpHash(); 158 | hash.set('/a/:b/c', routeHandler); 159 | 160 | // Act 161 | var rootResult = hash.get('/a/b/'); 162 | var staticResult = hash.get('/a/b/foo'); 163 | 164 | // Assert 165 | assert.strictEqual(staticResult.handler, null); 166 | assert.strictEqual(rootResult.handler, null); 167 | assert.end(); 168 | }); 169 | 170 | test('conflicting root exception', function (assert) { 171 | // Arrage 172 | function routeHandler() {} 173 | 174 | var hash = HttpHash(); 175 | hash.set('/', routeHandler); 176 | 177 | // Act 178 | var exception; 179 | 180 | try { 181 | hash.set('/', routeHandler); 182 | } catch (e) { 183 | exception = e; 184 | } 185 | 186 | // Assert 187 | assert.ok(exception); 188 | assert.strictEqual(exception.message, 'Route conflict'); 189 | assert.strictEqual(exception.attemptedPath, '/'); 190 | assert.strictEqual(exception.conflictPath, '/'); 191 | assert.end(); 192 | }); 193 | 194 | test('conflicting static route exception', function (assert) { 195 | // Arrage 196 | function routeHandler() {} 197 | 198 | var hash = HttpHash(); 199 | hash.set('/test', routeHandler); 200 | 201 | // Act 202 | var exception; 203 | 204 | try { 205 | hash.set('/test', routeHandler); 206 | } catch (e) { 207 | exception = e; 208 | } 209 | 210 | // Assert 211 | assert.ok(exception); 212 | assert.strictEqual(exception.message, 'Route conflict'); 213 | assert.strictEqual(exception.attemptedPath, '/test'); 214 | assert.strictEqual(exception.conflictPath, '/test/'); 215 | assert.end(); 216 | }); 217 | 218 | test('conflicting variable route exception', function (assert) { 219 | // Arrage 220 | function routeHandler() {} 221 | 222 | var hash = HttpHash(); 223 | hash.set('/:test', routeHandler); 224 | 225 | // Act 226 | var exception; 227 | 228 | try { 229 | hash.set('/:test', routeHandler); 230 | } catch (e) { 231 | exception = e; 232 | } 233 | 234 | // Assert 235 | assert.ok(exception); 236 | assert.strictEqual(exception.message, 'Route conflict'); 237 | assert.strictEqual(exception.message, 'Route conflict'); 238 | assert.strictEqual(exception.attemptedPath, '/:test'); 239 | assert.strictEqual(exception.conflictPath, '/:test/'); 240 | assert.end(); 241 | }); 242 | 243 | 244 | test('nesting routes', function (assert) { 245 | // Arrange 246 | var hash = HttpHash(); 247 | 248 | var firstRoutes = [ 249 | '/', 250 | '/test', 251 | '/:test' 252 | ]; 253 | 254 | var secondRoutes = [ 255 | '/', 256 | '/var', 257 | '/:var' 258 | ]; 259 | 260 | var conflicts = []; 261 | 262 | for (var i = 0; i < firstRoutes.length; i++) { 263 | for (var j = 0; j < secondRoutes.length; j++) { 264 | try { 265 | hash.set(firstRoutes[i] + secondRoutes[j], i + ',' + j); 266 | } catch (e) { 267 | conflicts.push(i + ',' + j); 268 | } 269 | } 270 | } 271 | 272 | // Act 273 | var results = {}; 274 | 275 | for (var k = 0; k < firstRoutes.length; k++) { 276 | for (var l = 0; l < secondRoutes.length; l++) { 277 | results[k + ',' + l] = hash.get(firstRoutes[k] + secondRoutes[l]); 278 | } 279 | } 280 | 281 | // Assert 282 | for (var m = 0; m < firstRoutes.length; m++) { 283 | for (var n = 0; n < secondRoutes.length; n++) { 284 | var index = m + ',' + n; 285 | 286 | if (conflicts.indexOf(index) >= 0) { 287 | continue; 288 | } 289 | 290 | assert.strictEqual(results[index].handler, index); 291 | } 292 | } 293 | 294 | // there should be 3 conflicts violation of variable name 295 | assert.strictEqual(conflicts.length, 3); 296 | 297 | assert.strictEqual(conflicts[0], '2,0'); 298 | assert.strictEqual(conflicts[1], '2,1'); 299 | assert.strictEqual(conflicts[2], '2,2'); 300 | assert.end(); 301 | }); 302 | 303 | test('deep route with splat test', function (assert) { 304 | // Arrange 305 | var hash = HttpHash(); 306 | 307 | function routeHandler() {} 308 | 309 | hash.set('/a/:varA/b/:varB/c/*', routeHandler); 310 | 311 | var expectedParams = { 312 | varA: '123456', 313 | varB: 'testing' 314 | }; 315 | 316 | var expectedSplat = 'kersplat'; 317 | 318 | // Act 319 | var result = hash.get('/a/123456///b///testing/c/kersplat'); 320 | 321 | // Assert 322 | assert.strictEqual(result.src, '/a/:varA/b/:varB/c/*'); 323 | assert.strictEqual(result.handler, routeHandler); 324 | assert.strictEqual(result.splat, expectedSplat); 325 | assert.deepEqual(result.params, expectedParams); 326 | assert.end(); 327 | }); 328 | 329 | test('splat in the middle causes splat error', function (assert) { 330 | // Arrange 331 | var hash = HttpHash(); 332 | 333 | // Act 334 | var exception; 335 | 336 | try { 337 | hash.set('/a/*/b'); 338 | } catch (e) { 339 | exception = e; 340 | } 341 | 342 | // Assert 343 | assert.ok(exception); 344 | assert.strictEqual( 345 | exception.message, 346 | 'The splat * must be the last segment of the path' 347 | ); 348 | assert.strictEqual(exception.pathname, '/a/*/b'); 349 | assert.end(); 350 | }); 351 | 352 | test('static routes work on splat nodes', function (assert) { 353 | // Arrange 354 | var hash = HttpHash(); 355 | function splatHandler() {} 356 | function staticHandler() {} 357 | 358 | hash.set('*', splatHandler); 359 | hash.set('/static', staticHandler); 360 | 361 | // Act 362 | var splatResult = hash.get('/testing'); 363 | var staticResult = hash.get('/static'); 364 | 365 | // Assert 366 | assert.strictEqual(splatResult.src, '*'); 367 | assert.strictEqual(splatResult.handler, splatHandler); 368 | assert.strictEqual(splatResult.splat, 'testing'); 369 | assert.deepEqual(splatResult.params, {}); 370 | 371 | assert.strictEqual(staticResult.src, '/static'); 372 | assert.strictEqual(staticResult.handler, staticHandler); 373 | assert.strictEqual(staticResult.splat, null); 374 | assert.deepEqual(staticResult.params, {}); 375 | 376 | assert.end(); 377 | }); 378 | 379 | test('vairable routes do not work on splat nodes', function (assert) { 380 | // Arrange 381 | var hash = HttpHash(); 382 | 383 | function splatHandler() {} 384 | hash.set('*', splatHandler); 385 | 386 | function variableHandler() {} 387 | 388 | // Act 389 | var exception; 390 | 391 | try { 392 | hash.set('/:var', variableHandler); 393 | } catch (e) { 394 | exception = e; 395 | } 396 | 397 | // Assert 398 | assert.ok(exception); 399 | assert.strictEqual(exception.message, 'Route conflict'); 400 | assert.strictEqual(exception.attemptedPath, '/:var'); 401 | assert.strictEqual(exception.conflictPath, '/*'); 402 | assert.end(); 403 | }); 404 | 405 | test('splat routes do not work on variable nodes', function (assert) { 406 | // Arrange 407 | var hash = HttpHash(); 408 | 409 | function splatHandler() {} 410 | hash.set('/:var', splatHandler); 411 | 412 | function variableHandler() {} 413 | 414 | // Act 415 | var exception; 416 | 417 | try { 418 | hash.set('*', variableHandler); 419 | } catch (e) { 420 | exception = e; 421 | } 422 | 423 | // Assert 424 | assert.ok(exception); 425 | assert.strictEqual(exception.message, 'Route conflict'); 426 | assert.strictEqual(exception.attemptedPath, '*'); 427 | assert.strictEqual(exception.conflictPath, '/:var/'); 428 | assert.end(); 429 | }); 430 | 431 | test('does not conflict with prototype', function (assert) { 432 | // Arrange 433 | var hash = HttpHash(); 434 | 435 | function validHandler() {} 436 | 437 | hash.set('/toString/valueOf', validHandler); 438 | 439 | // Act 440 | var toStringResult = hash.get('toString'); 441 | var valueOfResult = hash.get('valueOf'); 442 | var pathResult = hash.get('/toString/valueOf'); 443 | 444 | // Assert 445 | assert.strictEqual(toStringResult.handler, null); 446 | assert.strictEqual(valueOfResult.handler, null); 447 | assert.strictEqual(pathResult.handler, validHandler); 448 | assert.strictEqual(pathResult.src, '/toString/valueOf'); 449 | assert.end(); 450 | }); 451 | 452 | test('does not conflict with __proto__', function (assert) { 453 | // Arrage 454 | var hash = HttpHash(); 455 | 456 | function validHandler() {} 457 | function validSubHandler() {} 458 | 459 | hash.set('/__proto__', validHandler); 460 | hash.set('/__proto__/sub', validSubHandler); 461 | 462 | // Act 463 | var validResult = hash.get('/__proto__'); 464 | var validSubResult = hash.get('/__proto__/sub'); 465 | var invalidResult = hash.get('/__proto__/__proto__'); 466 | 467 | // Assert 468 | assert.strictEqual(validResult.handler, validHandler); 469 | assert.strictEqual(validResult.src, '/__proto__'); 470 | assert.strictEqual(validSubResult.handler, validSubHandler); 471 | assert.strictEqual(validSubResult.src, '/__proto__/sub'); 472 | assert.strictEqual(invalidResult.handler, null); 473 | assert.end(); 474 | }); 475 | 476 | test('root splat matches all', function (assert) { 477 | // Arrage 478 | var hash = HttpHash(); 479 | 480 | function validHandler() {} 481 | 482 | hash.set('*', validHandler); 483 | 484 | // Act 485 | var validEmptyResult = hash.get(''); 486 | var validRootResult = hash.get('/'); 487 | var validNestedResult = hash.get('/a/b/c'); 488 | 489 | // Assert 490 | assert.strictEqual(validEmptyResult.handler, validHandler); 491 | assert.strictEqual(validRootResult.handler, validHandler); 492 | assert.strictEqual(validNestedResult.handler, validHandler); 493 | assert.end(); 494 | }); 495 | --------------------------------------------------------------------------------