├── .github └── FUNDING.yml ├── .gitignore ├── lib ├── is-token-static.js ├── compare-dynamic-routes.js ├── resolve-path-meta.js └── ensure-path.js ├── .lint ├── .travis.yml ├── test ├── lib │ ├── is-token-static.js │ ├── compare-dynamic-routes.js │ ├── resolve-path-meta.js │ └── ensure-path.js ├── index.js └── nest.js ├── CHANGES ├── package.json ├── LICENSE ├── nest.js ├── CHANGELOG.md ├── README.md └── index.js /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: medikoo 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /node_modules 3 | /npm-debug.log 4 | /.lintcache 5 | -------------------------------------------------------------------------------- /lib/is-token-static.js: -------------------------------------------------------------------------------- 1 | // Wether path token is static or is dynamic (is regexp) 2 | 3 | 'use strict'; 4 | 5 | module.exports = RegExp.prototype.test.bind(/^[a-z0-9\-]+$/); 6 | -------------------------------------------------------------------------------- /.lint: -------------------------------------------------------------------------------- 1 | @root 2 | 3 | module 4 | 5 | tabs 6 | indent 2 7 | maxlen 100 8 | 9 | ass 10 | continue 11 | evil 12 | forin 13 | nomen 14 | plusplus 15 | stupid 16 | vars 17 | 18 | predef+ setTimeout 19 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false # http://docs.travis-ci.com/user/workers/container-based-infrastructure/ 2 | language: node_js 3 | node_js: 4 | - 4 5 | - 6 6 | - 7 7 | 8 | notifications: 9 | email: 10 | - medikoo+controller-router@medikoo.com 11 | 12 | script: "npm test && npm run lint" 13 | -------------------------------------------------------------------------------- /test/lib/is-token-static.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = function (t, a) { 4 | a(t('foo'), true); 5 | a(t('foo-bar'), true); 6 | a(t('0'), true); 7 | a(t('0foo'), true); 8 | a(t('0-2-3'), true); 9 | a(t('0-foo-3-bar'), true); 10 | a(t('foo-3-bar'), true); 11 | a(t('[a-z]+'), false); 12 | a(t('\\d+'), false); 13 | a(t('\\d{3}'), false); 14 | a(t('\\d{3,}'), false); 15 | }; 16 | -------------------------------------------------------------------------------- /test/lib/compare-dynamic-routes.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = function (t, a) { 4 | var conf1 = { 5 | matchPositionsLength: 3, 6 | matchPositions: [2, 4, 5] 7 | }; 8 | var conf2 = { 9 | matchPositionsLength: 2, 10 | matchPositions: [8, 9] 11 | }; 12 | var conf3 = { 13 | matchPositionsLength: 3, 14 | matchPositions: [1, 7, 9] 15 | }; 16 | a.deep([conf1, conf2, conf3].sort(t), [conf2, conf1, conf3]); 17 | }; 18 | -------------------------------------------------------------------------------- /lib/compare-dynamic-routes.js: -------------------------------------------------------------------------------- 1 | // Sorts internal dynamic route configurations by path specifity 2 | 3 | 'use strict'; 4 | 5 | module.exports = function (a, b) { 6 | var result; 7 | if (a.matchPositionsLength !== b.matchPositionsLength) { 8 | return a.matchPositionsLength - b.matchPositionsLength; 9 | } 10 | a.matchPositions.some(function (aPos, index) { 11 | var bPos = b.matchPositions[index]; 12 | if (aPos === bPos) return false; 13 | return (result = bPos - aPos); 14 | }); 15 | return result || 0; 16 | }; 17 | -------------------------------------------------------------------------------- /test/lib/resolve-path-meta.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = function (t, a) { 4 | a.deep(t('/'), { path: '/', static: true, tokens: [], matchPositions: [] }); 5 | a.deep(t('foo'), { path: 'foo', static: true, tokens: ['foo'], matchPositions: [] }); 6 | a.deep(t('foo/bar'), { path: 'foo/bar', static: true, tokens: ['foo', 'bar'], 7 | matchPositions: [] }); 8 | a.deep(t('foo/[a-z]+'), { path: 'foo/[a-z]+', static: false, 9 | tokens: ['foo', new RegExp(/^[a-z]+$/)], matchPositions: [1] }); 10 | a.deep(t('foo/[a-z]+/bar/\\d+'), { path: 'foo/[a-z]+/bar/\\d+', static: false, 11 | tokens: ['foo', new RegExp(/^[a-z]+$/), 'bar', new RegExp(/^\d+$/)], matchPositions: [1, 3] }); 12 | }; 13 | -------------------------------------------------------------------------------- /lib/resolve-path-meta.js: -------------------------------------------------------------------------------- 1 | // Resolve meta data for given path 2 | // Whether it's static, and if it's dynamic resolve dynamic tokens positions. 3 | 4 | 'use strict'; 5 | 6 | var isStatic = require('./is-token-static'); 7 | 8 | module.exports = function (path) { 9 | var data = { path: path, matchPositions: [] }; 10 | if (path === '/') { 11 | data.tokens = []; 12 | data.static = true; 13 | return data; 14 | } 15 | data.tokens = path.split('/'); 16 | data.tokens.forEach(function (token, index) { 17 | if (isStatic(token)) return; 18 | data.matchPositions.push(index); 19 | data.tokens[index] = new RegExp('^' + token + '$'); 20 | }); 21 | data.static = !data.matchPositions.length; 22 | return data; 23 | }; 24 | -------------------------------------------------------------------------------- /lib/ensure-path.js: -------------------------------------------------------------------------------- 1 | // Ensures that provided path applies to router convention 2 | 3 | 'use strict'; 4 | 5 | var stringifiable = require('es5-ext/object/validate-stringifiable-value') 6 | , customError = require('es5-ext/error/custom') 7 | , isStatic = require('./is-token-static') 8 | 9 | , stringify = JSON.stringify; 10 | 11 | module.exports = function (path) { 12 | path = stringifiable(path); 13 | if (path === '/') return path; 14 | path.split('/').forEach(function (token) { 15 | if (!token) throw customError("Invalid path " + stringify(path), 'INVALID_PATH'); 16 | if (isStatic(token)) return; 17 | try { RegExp('^' + token + '$'); } catch (e) { 18 | throw customError("Invalid regular expression in path " + stringify(path), 19 | 'INVALID_PATH_REGEX'); 20 | } 21 | }); 22 | return path; 23 | }; 24 | -------------------------------------------------------------------------------- /test/lib/ensure-path.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = function (t, a) { 4 | a.throws(function () { t(''); }, 'INVALID_PATH'); 5 | a.throws(function () { t('//'); }, 'INVALID_PATH'); 6 | a.throws(function () { t('/foo/'); }, 'INVALID_PATH'); 7 | a.throws(function () { t('/foo'); }, 'INVALID_PATH'); 8 | a.throws(function () { t('foo/'); }, 'INVALID_PATH'); 9 | a.throws(function () { t('foo/bar/'); }, 'INVALID_PATH'); 10 | a.throws(function () { t('/foo/bar'); }, 'INVALID_PATH'); 11 | a.throws(function () { t('foo//bar'); }, 'INVALID_PATH'); 12 | a.throws(function () { t('foo/*/bar'); }, 'INVALID_PATH_REGEX'); 13 | a(t('/'), '/'); 14 | a(t('foo'), 'foo'); 15 | a(t('foo/bar'), 'foo/bar'); 16 | a(t('foo/bar/loerm'), 'foo/bar/loerm'); 17 | a(t('[a-z]+'), '[a-z]+'); 18 | a(t('[a-z]+/[a-z]+'), '[a-z]+/[a-z]+'); 19 | a(t('foo/[a-z]+/bar'), 'foo/[a-z]+/bar'); 20 | }; 21 | -------------------------------------------------------------------------------- /CHANGES: -------------------------------------------------------------------------------- 1 | New changes are covered in CHANGELOG.md 2 | 3 | v3.2.0 -- 2017.03.31 4 | * On overlapped async match calls, we cancel (reject) those shadowed instead of returning false 5 | (this gave wrong message to higher level logic) 6 | 7 | v3.1.0 -- 2017.03.27 8 | * Introduce `promiseResultImplementation` option, thanks to which we can ensure consistent result 9 | types 10 | 11 | v3.0.0 -- 2017.03.20 12 | * Support asynchronous `match` (it may now return promise) 13 | * Do not try/catch eventual controller crashes (to avoid broken stack trace's). 14 | Instead of on error, expose the route details on router instance 15 | 16 | v2.0.0 -- 2015.06.25 17 | * Convert utility into more natural ControllerRouter class form 18 | * Improve specifity handling in dynamic routes resolution 19 | * Improve documentation 20 | * Rename internal modules to more corresponding names 21 | * Modularize configuration resolution, so it can be adapted by extensions 22 | 23 | v1.0.0 -- 2015.06.23 24 | * Initial 25 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "controller-router", 3 | "version": "3.9.3", 4 | "description": "URL to controller, router", 5 | "author": "Mariusz Nowak (http://www.medikoo.com/)", 6 | "keywords": [ 7 | "controller", 8 | "router", 9 | "url" 10 | ], 11 | "repository": { 12 | "type": "git", 13 | "url": "git://github.com/medikoo/controller-router.git" 14 | }, 15 | "dependencies": { 16 | "d": "1", 17 | "es5-ext": "^0.10.21", 18 | "event-emitter": "^0.3.5" 19 | }, 20 | "devDependencies": { 21 | "plain-promise": "^0.1.1", 22 | "tad": "^0.2.7", 23 | "xlint": "^0.2.2", 24 | "xlint-jslint-medikoo": "^0.1.4" 25 | }, 26 | "scripts": { 27 | "lint": "node node_modules/xlint/bin/xlint --linter=node_modules/xlint-jslint-medikoo/index.js --no-cache --no-stream", 28 | "lint-console": "node node_modules/xlint/bin/xlint --linter=node_modules/xlint-jslint-medikoo/index.js --watch", 29 | "test": "node node_modules/tad/bin/tad" 30 | }, 31 | "license": "MIT" 32 | } 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (C) 2015-2017 Mariusz Nowak (www.medikoo.com) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /nest.js: -------------------------------------------------------------------------------- 1 | // Generates new routes map, where provided routes are nested against extra url 2 | 3 | 'use strict'; 4 | 5 | var forEach = require('es5-ext/object/for-each') 6 | , normalizeOptions = require('es5-ext/object/normalize-options') 7 | , callable = require('es5-ext/object/valid-callable') 8 | , ensurePath = require('./lib/ensure-path') 9 | , resolvePathMeta = require('./lib/resolve-path-meta') 10 | 11 | , slice = Array.prototype.slice, apply = Function.prototype.apply, create = Object.create; 12 | 13 | module.exports = function (path, nestedRoutes/*, match*/) { 14 | var routes = create(null), match, pathData; 15 | path = ensurePath(path); 16 | pathData = resolvePathMeta(path); 17 | if (!pathData.static) match = callable(arguments[2]); 18 | forEach(nestedRoutes, function (conf, nestedPath) { 19 | var nestedMatch; 20 | if (typeof conf === 'function') conf = { controller: conf }; 21 | else if (conf === true) conf = {}; 22 | else conf = normalizeOptions(conf); 23 | routes[(nestedPath === '/') ? path : (path + '/' + nestedPath)] = conf; 24 | if (match) { 25 | if (conf.match) { 26 | nestedMatch = conf.match; 27 | conf.match = function () { 28 | return apply.call(match, this, slice.call(arguments, 0, pathData.matchPositions.length)) 29 | && apply.call(nestedMatch, this, slice.call(arguments, pathData.matchPositions.length)); 30 | }; 31 | } else { 32 | conf.match = match; 33 | } 34 | } 35 | }); 36 | return routes; 37 | }; 38 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. 4 | 5 | 6 | ## [3.9.3](https://github.com/medikoo/controller-router/compare/v3.9.2...v3.9.3) (2017-05-24) 7 | 8 | 9 | ### Bug Fixes 10 | 11 | * circular lastRouteData handling ([f2cd425](https://github.com/medikoo/controller-router/commit/f2cd425)) 12 | 13 | 14 | 15 | 16 | ## [3.9.2](https://github.com/medikoo/controller-router/compare/v3.9.1...v3.9.2) (2017-05-24) 17 | 18 | 19 | ### Bug Fixes 20 | 21 | * ensure error.routeData in case of sync errors ([ce3d8fe](https://github.com/medikoo/controller-router/commit/ce3d8fe)) 22 | 23 | 24 | 25 | 26 | ## [3.9.1](https://github.com/medikoo/controller-router/compare/v3.9.0...v3.9.1) (2017-05-24) 27 | 28 | 29 | ### Bug Fixes 30 | 31 | * ensure access to routeData in case of error ([f6991d8](https://github.com/medikoo/controller-router/commit/f6991d8)) 32 | 33 | 34 | 35 | 36 | # [3.9.0](https://github.com/medikoo/controller-router/compare/v3.8.0...v3.9.0) (2017-05-23) 37 | 38 | 39 | ### Features 40 | 41 | * move _eventProto to router.eventProto (public API) ([4f0eb1d](https://github.com/medikoo/controller-router/commit/4f0eb1d)) 42 | 43 | 44 | 45 | 46 | # [3.8.0](https://github.com/medikoo/controller-router/compare/v3.7.1...v3.8.0) (2017-05-23) 47 | 48 | 49 | ### Features 50 | 51 | * make router an emitter, emit route:before event ([86c37a9](https://github.com/medikoo/controller-router/commit/86c37a9)) 52 | 53 | 54 | 55 | 56 | ## [3.7.1](https://github.com/medikoo/controller-router/compare/v3.7.0...v3.7.1) (2017-05-19) 57 | 58 | 59 | ### Bug Fixes 60 | 61 | * ensure expected promise results from match ([f1ff256](https://github.com/medikoo/controller-router/commit/f1ff256)) 62 | 63 | 64 | 65 | 66 | # [3.7.0](https://github.com/medikoo/controller-router/compare/v3.6.0...v3.7.0) (2017-05-17) 67 | 68 | 69 | ### Features 70 | 71 | * ensure result is of requested promise implementation type ([f857d5b](https://github.com/medikoo/controller-router/commit/f857d5b)) 72 | 73 | 74 | 75 | 76 | # [3.6.0](https://github.com/medikoo/controller-router/compare/v3.5.1...v3.6.0) (2017-05-17) 77 | 78 | 79 | ### Features 80 | 81 | * auto bind `routeEvent` method ([7fb6f3b](https://github.com/medikoo/controller-router/commit/7fb6f3b)) 82 | 83 | 84 | 85 | 86 | ## [3.5.1](https://github.com/medikoo/controller-router/compare/v3.5.0...v3.5.1) (2017-05-16) 87 | 88 | 89 | ### Bug Fixes 90 | 91 | * do not break ES5 support ([5990338](https://github.com/medikoo/controller-router/commit/5990338)) 92 | 93 | 94 | 95 | 96 | # [3.5.0](https://github.com/medikoo/controller-router/compare/v3.4.1...v3.5.0) (2017-05-16) 97 | 98 | 99 | ### Features 100 | 101 | * ensure to expose resolved result ([bd3bf16](https://github.com/medikoo/controller-router/commit/bd3bf16)) 102 | 103 | 104 | 105 | 106 | ## [3.4.1](https://github.com/medikoo/controller-router/compare/v3.4.0...v3.4.1) (2017-05-16) 107 | 108 | 109 | ### Bug Fixes 110 | 111 | * ensure promise result in case of rejections ([3d8353b](https://github.com/medikoo/controller-router/commit/3d8353b)) 112 | 113 | 114 | 115 | 116 | # [3.4.0](https://github.com/medikoo/controller-router/compare/v3.3.0...v3.4.0) (2017-05-16) 117 | 118 | 119 | ### Features 120 | 121 | * auto bind `route` method ([c7433c7](https://github.com/medikoo/controller-router/commit/c7433c7)) 122 | 123 | 124 | 125 | 126 | # [3.3.0](https://github.com/medikoo/controller-router/compare/v3.2.0...v3.3.0) (2017-05-15) 127 | 128 | 129 | ### Features 130 | 131 | * resolve routeEvent after all promises resolve ([19ed04c](https://github.com/medikoo/controller-router/commit/19ed04c)) 132 | 133 | ## Old Changelog 134 | 135 | See `CHANGES` -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var clear = require('es5-ext/array/#/clear') 4 | , assign = require('es5-ext/object/assign') 5 | , Promise = require('plain-promise'); 6 | 7 | module.exports = function (T, a, d) { 8 | var called = [], obj = {}, conf, event = {}, router; 9 | 10 | a.h1("Ensure routes"); 11 | conf = {}; 12 | a(T.ensureRoutes(conf), conf); 13 | a.throws(function () { T.ensureRoutes('foo'); }, TypeError); 14 | conf = { 15 | foo: function () {}, 16 | 'bar/dwa': function () {}, 17 | 'elo/[a-z]+': { 18 | match: function (a1) {}, 19 | controller: function () {} 20 | }, 21 | 'elo/[a-z]+/bar/[0-9]+': { 22 | match: function (a1, a2) {}, 23 | controller: function () {} 24 | }, 25 | 'elo/dwa': function () {}, 26 | elo: function () {}, 27 | fiszka: function () {} 28 | }; 29 | a(T.ensureRoutes(conf), conf); 30 | conf.marko = {}; 31 | a.throws(function () { T.ensureRoutes(conf); }, 'INVALID_CONTROLLER'); 32 | conf.marko = function () {}; 33 | T.ensureRoutes(conf); 34 | conf['marko/[a-z]+'] = { 35 | controller: function () {}, 36 | match: true 37 | }; 38 | a.throws(function () { T.ensureRoutes(conf); }, 'INVALID_MATCH'); 39 | conf['marko/[a-z]+'] = { 40 | controller: function () {}, 41 | match: function (a1) {} 42 | }; 43 | a(T.ensureRoutes(conf), conf); 44 | 45 | a.h1("Router"); 46 | router = new T(conf = { 47 | '/': function () { called.push('root'); }, 48 | foo: function () { called.push('foo'); }, 49 | 'bar/dwa': function () { called.push('bar/dwa'); return obj; }, 50 | 'elo/dwa': function () { called.push('elo/dwa'); }, 51 | 'elo/dwa/[a-z]+': { 52 | match: function (a1) { 53 | called.push('elo/dwa/*:match'); 54 | return a1 === 'foo'; 55 | }, 56 | controller: function () { called.push('elo/dwa/*:controller'); } 57 | }, 58 | 'elo/dwa/[a-z]+/foo/[a-z]+': { 59 | match: function (a1, a2) { 60 | called.push('elo/dwa/*/foo/*:match2'); 61 | return (a1 === 'foo') && (a2 === 'bar'); 62 | }, 63 | controller: function () { called.push('elo/dwa/*/foo/*:controller'); } 64 | }, 65 | marko: function () { called.push('marko'); }, 66 | 'elo/trzy': function () { called.push('elo/trzy'); }, 67 | 'elo/dwa/filo': function () { called.push('elo/dwa/filo'); } 68 | }); 69 | 70 | a.deep(router.routeEvent(event, '/'), { conf: conf['/'], result: undefined, event: event }); 71 | a.deep(called, ['root']); 72 | clear.call(called); 73 | 74 | var emitted; 75 | router.once('route:before', function (event) { emitted = event.path; }); 76 | a.deep(router.routeEvent(event, '/foo/'), { conf: conf.foo, result: undefined, event: event }); 77 | a(emitted, '/foo/'); 78 | a.deep(called, ['foo']); 79 | clear.call(called); 80 | 81 | a(router.routeEvent(event, 'miszka'), false); 82 | a.deep(called, []); 83 | 84 | a.deep(router.routeEvent(event, 'marko'), { conf: conf.marko, result: undefined, event: event }); 85 | a.deep(called, ['marko']); 86 | clear.call(called); 87 | 88 | a.deep(router.routeEvent(event, 'bar/dwa'), { conf: conf['bar/dwa'], result: obj, event: event }); 89 | a.deep(called, ['bar/dwa']); 90 | clear.call(called); 91 | 92 | a.deep(router.routeEvent(event, 'elo/dwa'), 93 | { conf: conf['elo/dwa'], result: undefined, event: event }); 94 | a.deep(called, ['elo/dwa']); 95 | clear.call(called); 96 | 97 | a(router.routeEvent(event, 'elo/dwa/marko'), false); 98 | a.deep(called, ['elo/dwa/*:match']); 99 | clear.call(called); 100 | 101 | a.deep(router.routeEvent(event, 'elo/dwa/foo'), 102 | { conf: conf['elo/dwa/[a-z]+'], result: undefined, event: event }); 103 | a.deep(called, ['elo/dwa/*:match', 'elo/dwa/*:controller']); 104 | clear.call(called); 105 | 106 | a(router.routeEvent(event, 'elo/dwa/foo/foo/ilo'), false); 107 | a.deep(called, ['elo/dwa/*/foo/*:match2']); 108 | clear.call(called); 109 | 110 | a.deep(router.routeEvent(event, 'elo/dwa/foo/foo/bar'), 111 | { conf: conf['elo/dwa/[a-z]+/foo/[a-z]+'], result: undefined, event: event }); 112 | a.deep(called, ['elo/dwa/*/foo/*:match2', 'elo/dwa/*/foo/*:controller']); 113 | clear.call(called); 114 | 115 | a(router.routeEvent(event, 'elo/dwa/abla/bar'), false); 116 | a.deep(called, []); 117 | 118 | a(router.routeEvent(event, 'elo/dwa/foo/bar/miko'), false); 119 | a.deep(called, []); 120 | 121 | a.deep(router.routeEvent(event, 'elo/dwa/filo'), 122 | { conf: conf['elo/dwa/filo'], result: undefined, event: event }); 123 | a.deep(called, ['elo/dwa/filo']); 124 | clear.call(called); 125 | 126 | a.deep(router.routeEvent(event, 'elo/trzy'), 127 | { conf: conf['elo/trzy'], result: undefined, event: event }); 128 | a.deep(called, ['elo/trzy']); 129 | clear.call(called); 130 | 131 | a.h1("Promise"); 132 | clear.call(called); 133 | router = new T(conf = { 134 | '/': function () { called.push('root'); return 'foo'; }, 135 | 'matched/[0-9]+': { 136 | match: function (token) { 137 | this.token = token; 138 | return new Promise(function (resolve) { 139 | setTimeout(function () { resolve(true); }, token); 140 | }); 141 | }, 142 | controller: function () { 143 | called.push(this.token); 144 | } 145 | } 146 | }, { promiseResultImplementation: assign(function () {}, { resolve: function (result) { 147 | return { name: 'promise', result: result }; 148 | } }) }); 149 | 150 | var route = router.route; 151 | var result = route('/'); 152 | a.deep(result, { name: 'promise', 153 | result: { conf: conf['/'], result: 'foo', event: result.result.event } }); 154 | a.deep(called, ['root']); 155 | clear.call(called); 156 | 157 | var wasCalled = false; 158 | router.Promise = Promise; 159 | router.route('/matched/50').done(a.never, function (err) { 160 | wasCalled = true; 161 | a(err.code, 'OUTDATED_ROUTE_CALL'); 162 | }); 163 | router.route('/matched/100').done(function () { 164 | a(wasCalled, true); 165 | d(); 166 | }, a.never); 167 | }; 168 | -------------------------------------------------------------------------------- /test/nest.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var assign = require('es5-ext/object/assign') 4 | , clear = require('es5-ext/array/#/clear') 5 | , ControllerRouter = require('../'); 6 | 7 | module.exports = { 8 | Direct: function (t, a) { 9 | var called = [], obj = {}, conf, event = {}; 10 | var router = new ControllerRouter(t('milo/foo', conf = { 11 | '/': function () { called.push('root'); }, 12 | foo: function () { called.push('foo'); }, 13 | 'bar/dwa': function () { called.push('bar/dwa'); return obj; }, 14 | 'elo/dwa': function () { called.push('elo/dwa'); }, 15 | 'elo/dwa/[a-z]+': { 16 | match: function (a1) { 17 | called.push('elo/dwa/*:match'); 18 | return a1 === 'foo'; 19 | }, 20 | controller: function () { called.push('elo/dwa/*:controller'); } 21 | }, 22 | 'elo/dwa/[a-z]+/foo/[a-z]+': { 23 | match: function (a1, a2) { 24 | called.push('elo/dwa/*/foo/*:match2'); 25 | return (a1 === 'foo') && (a2 === 'bar'); 26 | }, 27 | controller: function () { called.push('elo/dwa/*/foo/*:controller'); } 28 | }, 29 | marko: function () { called.push('marko'); }, 30 | 'elo/trzy': function () { called.push('elo/trzy'); }, 31 | 'elo/dwa/filo': function () { called.push('elo/dwa/filo'); } 32 | })); 33 | 34 | a(router.routeEvent(event, '/'), false); 35 | 36 | a.deep(router.routeEvent(event, '/milo/foo/'), 37 | { conf: { controller: conf['/'] }, result: undefined, event: event }); 38 | a.deep(called, ['root']); 39 | clear.call(called); 40 | 41 | a.deep(router.routeEvent(event, '/milo/foo/foo/'), 42 | { conf: { controller: conf.foo }, result: undefined, event: event }); 43 | a.deep(called, ['foo']); 44 | clear.call(called); 45 | 46 | a(router.routeEvent(event, '/milo/foo/miszka'), false); 47 | a.deep(called, []); 48 | 49 | a.deep(router.routeEvent(event, '/milo/foo/marko'), 50 | { conf: { controller: conf.marko }, result: undefined, event: event }); 51 | a.deep(called, ['marko']); 52 | clear.call(called); 53 | 54 | a.deep(router.routeEvent(event, '/milo/foo/bar/dwa'), 55 | { conf: { controller: conf['bar/dwa'] }, result: obj, event: event }); 56 | a.deep(called, ['bar/dwa']); 57 | clear.call(called); 58 | 59 | a.deep(router.routeEvent(event, '/milo/foo/elo/dwa'), { conf: { controller: conf['elo/dwa'] }, 60 | result: undefined, event: event }); 61 | a.deep(called, ['elo/dwa']); 62 | clear.call(called); 63 | 64 | a(router.routeEvent(event, '/milo/foo/elo/dwa/marko'), false); 65 | a.deep(called, ['elo/dwa/*:match']); 66 | clear.call(called); 67 | 68 | a.deep(router.routeEvent(event, '/milo/foo/elo/dwa/foo'), 69 | { conf: conf['elo/dwa/[a-z]+'], result: undefined, event: event }); 70 | a.deep(called, ['elo/dwa/*:match', 'elo/dwa/*:controller']); 71 | clear.call(called); 72 | 73 | a(router.routeEvent(event, '/milo/foo/elo/dwa/foo/foo/ilo'), false); 74 | a.deep(called, ['elo/dwa/*/foo/*:match2']); 75 | clear.call(called); 76 | 77 | a.deep(router.routeEvent(event, '/milo/foo/elo/dwa/foo/foo/bar'), 78 | { conf: conf['elo/dwa/[a-z]+/foo/[a-z]+'], result: undefined, event: event }); 79 | a.deep(called, ['elo/dwa/*/foo/*:match2', 'elo/dwa/*/foo/*:controller']); 80 | clear.call(called); 81 | 82 | a(router.routeEvent(event, '/milo/foo/elo/dwa/abla/bar'), false); 83 | a.deep(called, []); 84 | 85 | a(router.routeEvent(event, '/milo/foo/elo/dwa/foo/bar/miko'), false); 86 | a.deep(called, []); 87 | 88 | a.deep(router.routeEvent(event, '/milo/foo/elo/dwa/filo'), 89 | { conf: { controller: conf['elo/dwa/filo'] }, result: undefined, event: event }); 90 | a.deep(called, ['elo/dwa/filo']); 91 | clear.call(called); 92 | 93 | a.deep(router.routeEvent(event, '/milo/foo/elo/trzy'), { conf: { controller: conf['elo/trzy'] }, 94 | result: undefined, event: event }); 95 | a.deep(called, ['elo/trzy']); 96 | clear.call(called); 97 | }, 98 | Match: function (t, a) { 99 | var called = [], obj = {}, conf, nestConf, event = {}; 100 | var router = new ControllerRouter(nestConf = t('milo/[a-z]+/foo', conf = { 101 | '/': function () { called.push('root'); }, 102 | foo: function () { called.push('foo'); }, 103 | 'bar/dwa': function () { called.push('bar/dwa'); return obj; }, 104 | 'elo/dwa': function () { called.push('elo/dwa'); }, 105 | 'elo/dwa/[a-z]+': { 106 | match: function (a1) { 107 | called.push('elo/dwa/*:match'); 108 | return a1 === 'foo'; 109 | }, 110 | controller: function () { called.push('elo/dwa/*:controller'); } 111 | }, 112 | 'elo/dwa/[a-z]+/foo/[a-z]+': { 113 | match: function (a1, a2) { 114 | called.push('elo/dwa/*/foo/*:match2'); 115 | return (a1 === 'foo') && (a2 === 'bar'); 116 | }, 117 | controller: function () { called.push('elo/dwa/*/foo/*:controller'); } 118 | }, 119 | marko: function () { called.push('marko'); }, 120 | 'elo/trzy': function () { called.push('elo/trzy'); }, 121 | 'elo/dwa/filo': function () { called.push('elo/dwa/filo'); } 122 | }, function (id) { return (id === 'elos'); })); 123 | 124 | a(router.routeEvent(event, '/'), false); 125 | 126 | a.deep(router.routeEvent(event, '/milo/binio/foo/'), false); 127 | 128 | a.deep(router.routeEvent(event, '/milo/elos/foo/'), { conf: { controller: conf['/'], 129 | match: nestConf['milo/[a-z]+/foo'].match }, result: undefined, event: event }); 130 | a.deep(called, ['root']); 131 | clear.call(called); 132 | 133 | a.deep(router.routeEvent(event, '/milo/elos/foo/foo/'), { conf: { controller: conf.foo, 134 | match: nestConf['milo/[a-z]+/foo/foo'].match }, result: undefined, event: event }); 135 | a.deep(called, ['foo']); 136 | clear.call(called); 137 | 138 | a(router.routeEvent(event, '/milo/elos/foo/miszka'), false); 139 | a.deep(called, []); 140 | 141 | a.deep(router.routeEvent(event, '/milo/elos/foo/marko'), { conf: { controller: conf.marko, 142 | match: nestConf['milo/[a-z]+/foo/marko'].match }, result: undefined, event: event }); 143 | a.deep(called, ['marko']); 144 | clear.call(called); 145 | 146 | a.deep(router.routeEvent(event, '/milo/elos/foo/bar/dwa'), { conf: { 147 | controller: conf['bar/dwa'], 148 | match: nestConf['milo/[a-z]+/foo/bar/dwa'].match 149 | }, result: obj, event: event }); 150 | a.deep(called, ['bar/dwa']); 151 | clear.call(called); 152 | 153 | a.deep(router.routeEvent(event, '/milo/elos/foo/elo/dwa'), { conf: { 154 | controller: conf['elo/dwa'], 155 | match: nestConf['milo/[a-z]+/foo/elo/dwa'].match 156 | }, result: undefined, event: event }); 157 | a.deep(called, ['elo/dwa']); 158 | clear.call(called); 159 | 160 | a(router.routeEvent(event, '/milo/elos/foo/elo/dwa/marko'), false); 161 | a.deep(called, ['elo/dwa/*:match']); 162 | clear.call(called); 163 | 164 | a.deep(router.routeEvent(event, '/milo/elos/foo/elo/dwa/foo'), 165 | { conf: assign({}, conf['elo/dwa/[a-z]+'], 166 | { match: nestConf['milo/[a-z]+/foo/elo/dwa/[a-z]+'].match }), 167 | result: undefined, event: event }); 168 | a.deep(called, ['elo/dwa/*:match', 'elo/dwa/*:controller']); 169 | clear.call(called); 170 | 171 | a(router.routeEvent(event, '/milo/elos/foo/elo/dwa/foo/foo/ilo'), false); 172 | a.deep(called, ['elo/dwa/*/foo/*:match2']); 173 | clear.call(called); 174 | 175 | a.deep(router.routeEvent(event, '/milo/elos/foo/elo/dwa/foo/foo/bar'), 176 | { conf: assign({}, conf['elo/dwa/[a-z]+/foo/[a-z]+'], 177 | { match: nestConf['milo/[a-z]+/foo/elo/dwa/[a-z]+/foo/[a-z]+'].match }), 178 | result: undefined, event: event }); 179 | a.deep(called, ['elo/dwa/*/foo/*:match2', 'elo/dwa/*/foo/*:controller']); 180 | clear.call(called); 181 | 182 | a(router.routeEvent(event, '/milo/elos/foo/elo/dwa/abla/bar'), false); 183 | a.deep(called, []); 184 | 185 | a(router.routeEvent(event, '/milo/elos/foo/elo/dwa/foo/bar/miko'), false); 186 | a.deep(called, []); 187 | 188 | a.deep(router.routeEvent(event, '/milo/elos/foo/elo/dwa/filo'), 189 | { conf: { controller: conf['elo/dwa/filo'], 190 | match: nestConf['milo/[a-z]+/foo/elo/dwa/filo'].match }, 191 | result: undefined, event: event }); 192 | a.deep(called, ['elo/dwa/filo']); 193 | clear.call(called); 194 | 195 | a.deep(router.routeEvent(event, '/milo/elos/foo/elo/trzy'), { conf: { 196 | controller: conf['elo/trzy'], 197 | match: nestConf['milo/[a-z]+/foo/elo/trzy'].match 198 | }, result: undefined, event: event }); 199 | a.deep(called, ['elo/trzy']); 200 | clear.call(called); 201 | } 202 | }; 203 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # controller-router 2 | ## URL to controller, router 3 | 4 | Basic example of router configuration: 5 | 6 | ```javascript 7 | var ControllerRouter = require('controler-router'); 8 | 9 | // Get router for provided routes 10 | var router = new ControllerRouter({ 11 | // '/' route 12 | '/': function () { 13 | console.log("Root!"); 14 | }, 15 | // '/foo' route 16 | 'foo': function () { 17 | console.log("Foo!"); 18 | }, 19 | // '/foo/bar' route 20 | 'foo/bar': function () { 21 | console.log("Foo, Bar!"); 22 | }, 23 | // '/lorem/*' dynamic route 24 | 'lorem/[0-9][0-9a-z]+': { 25 | match: function (id) { 26 | if (id !== '0abc') return false; 27 | // for demo purposes, pass only '0abc' 28 | this.name = id; 29 | }, 30 | controller: function () { 31 | console.log("Lorem, " + this.name + "!"); 32 | } 33 | } 34 | }); 35 | 36 | router.route('/'); // Calls "/" controller (logs "Root!") and returns route call event object 37 | router.route('/foo/'); // "Foo!" 38 | router.route('/not-existing/'); // Not found, returns false 39 | router.route('/foo/bar/'); // "Foo, Bar!" 40 | router.route('/lorem/elo'); // Not found, returns false 41 | router.route('/lorem/0abc'); // "Lorem, 0abc!" 42 | ``` 43 | 44 | ### Installation 45 | 46 | $ npm install controller-router 47 | 48 | ### API 49 | #### ControllerRouter constructor properties 50 | ##### ControllerRouter.ensureRoutes(routes) 51 | 52 | Validates provided routes configuration, it is also used internally on router initialization 53 | 54 | #### ControllerRouter initialization 55 | ##### new ControllerRouter(routes[, options]) 56 | 57 | ```javascript 58 | var ControllerRouter = require('controller-router'); 59 | 60 | var router = new ControllerRouter({ 61 | // .. routes configuration 62 | }); 63 | ``` 64 | 65 | ControllerRouter on initalizaton accepts [routes map](#routes-map-configuration), and eventual options: 66 | - __eventProto__ - Prototype for route events. If provided, then each event, will be an instance that inherits from this object. 67 | For more information about _event_ object, see [Handling of router function](https://github.com/medikoo/controller-router#handling-of-router-function) section. 68 | 69 | ###### Routes map configuration 70 | 71 | In routes map, _key_ is a path, and _value_ is a controller. Routes are defined as flat map, there are no nested route configurations. 72 | 73 | ####### Routes map: path keys 74 | 75 | A valid path key is a series of tokens separated with `/` character. Where a token for typical static path is built strictly out of `a-z, 0-9, -` characters set. 76 | 77 | We can also mix into path a dynamic path tokens, which we describe in regular expression format, but it is assumed that all tokens also resolve strictly to values built out of `a-z, 0-9, -` character set. 78 | 79 | Internally engine naturally distinguish between static and dynamic tokens on basis of fact that regular expression will use characters out of basic set. 80 | 81 | In addition to above, a `/` key is understood as root url. 82 | 83 | Examples of static path keys: 84 | - __/__ - root url, matches strictly `/` route 85 | - __foo__ - matches `/foo` or `/foo/` route 86 | - __foo/bar/elo__ - matches `/foo/bar/elo` or `/foo/bar/elo/` route 87 | 88 | Examples of dynamic path keys: 89 | - __[a-z]{3}__ - matches e.g. `/abc` `/zws/` `/aaa/`, won't match e.g. `/ab0` or `/abcd` 90 | - __user/[0-9][a-z0-9]{6}__ - matches e.g. `/user/0asd34d` or `/user/7few232` route 91 | - __lorem/[0-9][a-z0-9]{6}/foo/[a-z]{1,2}__ - matches e.g. `/lorem/0asd34d/foo.` or `/user/7few232` route 92 | 93 | Routes for dynamic path keys, can be combined with static that override them. e.g. we may have configuration for _user/[a-z]{3,10}_ and _user/mark_, and `/user/mark` url will be routed to __user/mark__ configuration, as it has higher specifity for given path 94 | 95 | ####### Routes map: controller values 96 | 97 | For static path keys, controllers may be direct functions e.g.: 98 | ```javascript 99 | 'foo/bar': function () { 100 | // controller body 101 | } 102 | ``` 103 | 104 | They can also be configured with objects which provide a `controller` property: 105 | ```javascript 106 | 'foo/bar': { 107 | controller: function () { 108 | // controller body 109 | } 110 | }; 111 | ``` 112 | 113 | Two of above configurations are equal in meaning. 114 | 115 | If path key contains dynamic tokens, then `match` function is required, and configuration must be configured as: 116 | 117 | ```javascript 118 | 'lorem/[0-9][a-z0-9]{6}/foo/[a-z]{1,2}': { 119 | match: function (token1, token2) { 120 | if (!loremExists(token1) || !fooExists(token2)) { 121 | // while tokens matched pattern, no corresponding entities were found 122 | return false; 123 | } 124 | this.lorem = resolveLorem(token1); 125 | this.foo = resolve(token2); 126 | return true; 127 | }, 128 | controller: function () { 129 | // controller body 130 | doSomethingWith(this.lorem, this.foo); 131 | } 132 | }; 133 | ``` 134 | 135 | `match` function would be invoked in same _event_ context as controller, and arguments it receives is resolved tokens from url which match all route regexp tokens. 136 | 137 | #### ControllerRouter instance properties 138 | ##### controllerRouter.route(path[, ...controllerArgs]) 139 | 140 | Resolves controller for given path, and if one is found, it is invoked. Additionally after a path argument, we can pass arguments for _controller_ function (mind that those arguments won't be provided to eventual _match_ function) 141 | 142 | For each method call, a new _event_ is created (as a plain object, or as an extension to provided at initialization `eventProto` object). 143 | It is used as a context for _match_ and _controller_ invocations, _event_ object should be used as a transport for values that we resolve at _match_ step, and want to access at _controller_ step. 144 | 145 | `router` method when invoked returns either `false` when no controller for given path was found, or in case of a valid route, a result object with following properties is returned: 146 | - `conf` a route configuration for chosen path (as it's provided on routes object) 147 | - `event`, an event for given router call 148 | - `result` a result value as returned by invoked controller 149 | 150 | If internally invoked controller function crashes, then `conf` and `event` objects, can be found as properties on error instance. 151 | 152 | ##### controllerRouter.routeEvent(event, path[, ...controllerArgs]) 153 | 154 | With `routeEvent` method we can force specific _event_ (controller context) for given route call. Aside of that it behaves exactly as `route` method. 155 | 156 | #### nestRoutes(path, routes[, match]) 157 | 158 | ```javascript 159 | var nestRoutes = require('controller-router/nest'); 160 | 161 | var routes = { 162 | bar: function barController() { ... } 163 | }; 164 | 165 | var nestedAgainstFoo = nestRoutes('foo', routes); 166 | console.log(nestedAgainstFoo); 167 | // { 'foo/bar': fuction barController() { ... } } 168 | 169 | var nestedAgainstUser = nestRoutes('user/[0-9][a-z0-9]{6}', routes, function (userId) { 170 | this.user = resolveUser(userId); 171 | }); 172 | console.log(nestedAgainsUser); 173 | // { 'user/[0-9][a-z0-9]{6}/bar': { 174 | // match: function (userId) {...} , 175 | // controller: function barController() { ... } 176 | // } } 177 | ``` 178 | 179 | Returns new routes map, where each route is additionally nested against provided path. 180 | Provided nest path can contain regExp tokens, in such case also `match` function must be passed. 181 | 182 | It's useful, when we have configured routes, which in some special cases we want to use against some nested route. 183 | 184 | ### Available Extensions 185 | 186 | __controller-router__ on its own is a generic utility and doesn't provide functionalities which you would need for certain use cases. Following is a list of extensions which address specific scenarios: 187 | 188 | - [post-controller-router](https://github.com/medikoo/post-controller-router#post-controller-router) - Router dedicated for update requests (e.g. form submissions in browsers, or POST requests on server-side) 189 | - [site-tree-router](https://github.com/medikoo/site-tree-router#site-tree-router) - A view engine router (to switch between pages in response to url changes in address bar) 190 | 191 | ### Tests [![Build Status](https://travis-ci.org/medikoo/controller-router.svg)](https://travis-ci.org/medikoo/controller-router) 192 | 193 | $ npm test 194 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | // ControllerRouter class 2 | 3 | 'use strict'; 4 | 5 | var includes = require('es5-ext/array/#/contains') 6 | , customError = require('es5-ext/error/custom') 7 | , identity = require('es5-ext/function/identity') 8 | , assign = require('es5-ext/object/assign') 9 | , isPromise = require('es5-ext/object/is-promise') 10 | , ensureCallable = require('es5-ext/object/valid-callable') 11 | , ensureObject = require('es5-ext/object/valid-object') 12 | , ensureStringifiable = require('es5-ext/object/validate-stringifiable-value') 13 | , forEach = require('es5-ext/object/for-each') 14 | , endsWith = require('es5-ext/string/#/ends-with') 15 | , ee = require('event-emitter') 16 | , d = require('d') 17 | , autoBind = require('d/auto-bind') 18 | , lazy = require('d/lazy') 19 | , compareDynamicRoutes = require('./lib/compare-dynamic-routes') 20 | , isStatic = require('./lib/is-token-static') 21 | , resolvePathMeta = require('./lib/resolve-path-meta') 22 | , ensurePath = require('./lib/ensure-path') 23 | 24 | , push = Array.prototype.push, slice = Array.prototype.slice, apply = Function.prototype.apply 25 | , create = Object.create, defineProperty = Object.defineProperty, stringify = JSON.stringify 26 | , routeCallIdIndex = 0; 27 | 28 | var ControllerRouter = module.exports = Object.defineProperties(function (routes/*, options*/) { 29 | var options; 30 | // Validate initialization 31 | if (!(this instanceof ControllerRouter)) return new ControllerRouter(routes, arguments[1]); 32 | options = Object(arguments[1]); 33 | this.constructor.ensureRoutes(routes, options); 34 | 35 | defineProperty(this, 'eventProto', 36 | d((options.eventProto != null) ? ensureObject(options.eventProto) : {})); 37 | 38 | defineProperty(this, 'routes', d(routes)); 39 | 40 | // Configure internal routes map 41 | forEach(this.constructor.normalizeRoutes(routes, options), function (conf, path) { 42 | var pathData = resolvePathMeta(path); 43 | if (pathData.static) { 44 | this._staticRoutes[path] = conf; 45 | return; 46 | } 47 | if (!this._dynamicRoutes[pathData.tokens.length]) { 48 | this._dynamicRoutes[pathData.tokens.length] = []; 49 | } 50 | pathData.conf = conf; 51 | pathData.matchPositionsLength = pathData.matchPositions.length; 52 | this._dynamicRoutes[pathData.tokens.length].push(pathData); 53 | }, this); 54 | forEach(this._dynamicRoutes, function (data) { data.sort(compareDynamicRoutes); }); 55 | 56 | // Whether route should resolve with promise or not 57 | if (options.promiseResultImplementation) { 58 | defineProperty(this, 'Promise', d(ensureCallable(options.promiseResultImplementation))); 59 | } 60 | }, { 61 | // Validates provided routes map 62 | ensureRoutes: d(function (routes) { 63 | forEach(ensureObject(routes), function (conf, path) { 64 | var isDynamic; 65 | ensureObject(conf); 66 | ensurePath(path); 67 | if (path !== '/') isDynamic = !path.split('/').every(isStatic); 68 | if (typeof conf === 'function') { 69 | if (isDynamic) { 70 | throw customError("Missing match function for " + stringify(path), 'MISSING_MATCH'); 71 | } 72 | return; 73 | } 74 | if (typeof conf.controller !== 'function') { 75 | throw customError("Invalid controller for " + stringify(path), 'INVALID_CONTROLLER'); 76 | } 77 | if (typeof conf.match !== 'function') { 78 | if (isDynamic) { 79 | throw customError("Invalid match function for " + stringify(path), 'INVALID_MATCH'); 80 | } 81 | return; 82 | } 83 | if (!isDynamic) { 84 | throw customError("Missing regular expression in path " + stringify(path), 85 | 'MISSING_PATH_REGEX'); 86 | } 87 | }); 88 | return routes; 89 | }), 90 | // Normalizes routes to bare ControllerRouter format. If ControllerRouter extension 91 | // provides more sophisticated routes format, this should be a function which transforms it 92 | // directly to map as understood by ControllerRouter 93 | normalizeRoutes: d(identity) 94 | }); 95 | 96 | ee(Object.defineProperties(ControllerRouter.prototype, assign({ 97 | _resolveResult: d(function (routeData) { 98 | var routeResult = routeData; 99 | if (routeData && isPromise(routeData.result)) { 100 | routeResult = routeData.result.then(function (resolvedResult) { 101 | routeData.result = resolvedResult; 102 | return routeData; 103 | }, function (error) { 104 | delete routeData.result; 105 | routeData.error = error; 106 | error.routeData = routeData; 107 | throw error; 108 | }); 109 | } 110 | return this.Promise ? this.Promise.resolve(routeResult) : routeResult; 111 | }), 112 | _resolveController: d(function (fn) { 113 | var routeData = this.lastRouteData; 114 | if (!this.Promise) { 115 | routeData.result = fn(); 116 | } else { 117 | try { 118 | routeData.result = fn(); 119 | this.lastRouteData = routeData; 120 | } catch (e) { 121 | this.lastRouteData = routeData; 122 | routeData.error = e; 123 | e.routeData = routeData; 124 | return this.Promise.reject(e); 125 | } 126 | } 127 | return this._resolveResult(routeData); 128 | }) 129 | 130 | }, lazy({ 131 | // Internal map of dynamic routes (those that contain regexp tokens) 132 | _dynamicRoutes: d(function () { return create(null); }), 133 | // Internal map of static routes (no regexp tokens) 134 | _staticRoutes: d(function () { return create(null); }) 135 | }), autoBind({ 136 | // Routes path to controller and provides an event to be used for controller invocation 137 | routeEvent: d(function (event, path/*, …controllerArgs*/) { 138 | var pathTokens, controllerArgs = slice.call(arguments, 2), conf, initConf, controller 139 | , asyncResult, callId = ++routeCallIdIndex; 140 | 141 | ensureObject(event); 142 | path = ensureStringifiable(path); 143 | 144 | this.emit("route:before", { event: event, path: path, args: controllerArgs }); 145 | 146 | // Preprepare route data 147 | this.lastRouteData = { event: event }; 148 | 149 | // Do not proceed for no path 150 | if (!path) return this._resolveResult(false); 151 | 152 | // Resolve path for route resolution 153 | if (path[0] === '/') path = path.slice(1); 154 | if (endsWith.call(path, '/')) path = path.slice(0, -1); 155 | event.path = path || '/'; 156 | 157 | // Handle eventual static path 158 | conf = this._staticRoutes[path || '/']; 159 | if (conf) { 160 | initConf = this.routes[path || '/']; 161 | controller = conf.controller || conf; 162 | this.lastRouteData.conf = initConf; 163 | return this._resolveController(apply.bind(controller, event, controllerArgs)); 164 | } 165 | 166 | // Handle eventual dynamic paths 167 | pathTokens = path.split('/'); 168 | if (!this._dynamicRoutes[pathTokens.length]) return this._resolveResult(false); 169 | this._dynamicRoutes[pathTokens.length].some(function (data) { 170 | var args = [], matchResult; 171 | 172 | // Check whether path matches 173 | if (!data.tokens.every(function (token, index) { 174 | var pathToken = pathTokens[index]; 175 | if (includes.call(data.matchPositions, index)) { 176 | if (!token.test(pathToken)) return false; 177 | args.push(pathToken); 178 | return true; 179 | } 180 | return (token === pathToken); 181 | })) { 182 | return false; 183 | } 184 | 185 | // Path matches, now resolve with `match` function 186 | matchResult = data.conf.match.apply(event, args); 187 | if (isPromise(matchResult)) { 188 | asyncResult = matchResult.then(function (isMatch) { 189 | if (!isMatch) return false; 190 | this.lastRouteData = { 191 | event: event, 192 | conf: this.routes[data.path] 193 | }; 194 | if (callId !== routeCallIdIndex) { 195 | // Cancel route (as promises are concerned we need to reject) 196 | throw customError("Route cancelled, as next one took action", 'OUTDATED_ROUTE_CALL'); 197 | } 198 | this.lastRouteData.result = apply.call(data.conf.controller, event, controllerArgs); 199 | return this._resolveResult(this.lastRouteData); 200 | }.bind(this)); 201 | } else if (matchResult) { 202 | conf = data.conf; 203 | initConf = this.routes[data.path]; 204 | } 205 | return true; 206 | }, this); 207 | 208 | // If `match` is asynchronous return promise 209 | if (asyncResult) return this.Promise ? this.Promise.resolve(asyncResult) : asyncResult; 210 | if (!conf) return this._resolveResult(false); 211 | this.lastRouteData.conf = initConf; 212 | return this._resolveController(apply.bind(conf.controller, event, controllerArgs)); 213 | }), 214 | 215 | // Routes path to controller 216 | route: d(function (path/*, …controllerArgs*/) { 217 | var args = [create(this.eventProto)]; 218 | push.apply(args, arguments); 219 | return this.routeEvent.apply(this, args); 220 | }) 221 | })))); 222 | --------------------------------------------------------------------------------