Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP

Loading…

Метод генерации урла ns.router.generateUrl #154

Merged
merged 6 commits into from

2 participants

@chestozo
Owner

ns.router.generateUrl(page, params) и тесты к нему.
/сс @doochik @edoroshenko

@chestozo
Owner

Will Fix #123

@chestozo
Owner

ща, попробую схлопнуть всё в один коммит )

@chestozo
Owner

Готово, можно review-ить / merge-ить

@chestozo
Owner

Замёрж меня )

@doochik
Owner

Добавил падающий тест

@chestozo
Owner

Ready to merge

@doochik
Owner

Может добавим еще поддержку rewriteUrl ?

Есть роут

routes = {
  rewriteUrl: {
     '/shortcut': '/page/1'
  },
  route: {
     '/page/{id:int}': 'page'
  }

}

Сейчас ns.router.generateUrl('page', {id: 1}) сгенерит /page/1, а хочется /shortcut
По сути надо всего лишь после генерации прогнать урл через обратный хеш от rewriteUrl

@chestozo
Owner

Договорились о следующем:
1) ns.router.generateUrl(id, params) генерит урл, все параметры, которые не попали в урл сохраняются отдельным списком (см. дальше)
2) сгенерённый урл прогоняется через rewriteUrl (в цикле, пока не останется rewrite-ов для текущего урла)
3) к полученному урлу дописывают параметры из п.1 в виде querystring

@chestozo
Owner

Ммм... а у нас ведь ещё есть rewriteParams, его не трогаем? Это ведь функция? )

@chestozo
Owner

И снова всё готово )
Только нужно прояснить вопрос про rewriteParams )

@doochik doochik merged commit 430f03c into master
@chestozo
Owner

Откатили и будем дорабатывать в #160

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
This page is out of date. Refresh to see the latest.
View
243 src/ns.router.js
@@ -46,14 +46,15 @@ ns.router = function(url) {
var r = route.regexp.exec(url);
if (r) {
- var tokens = route.tokens;
- var defaults = route.defaults;
+ var rparams = route.params;
var params = {};
- // Вытаскиваем параметры из основной части урла. Имена параметров берем из массива tokens.
- var l = tokens.length;
+ // Вытаскиваем параметры из основной части урла.
+ var l = rparams.length;
+ var rparam;
for (var k = 0; k < l; k++) {
- params[ tokens[k] ] = r[ k + 1 ] || defaults[ tokens[k] ];
+ rparam = rparams[k];
+ params[rparam.name] = r[k + 1] || rparam.default_value;
}
// Смотрим, есть ли дополнительные get-параметры, вида ?param1=value1&param2=value2...
@@ -83,15 +84,6 @@ ns.router = function(url) {
};
/**
- * Generate url.
- * @param {string} url Relative url.
- * @return {String} Valid url that takes into consideration baseDir.
- */
-ns.router.url = function(url) {
- return ns.router.baseDir + url;
-};
-
-/**
* Inititialize ns.router, compiles defined routes.
*/
ns.router.init = function() {
@@ -102,76 +94,215 @@ ns.router.init = function() {
_routes.rewriteUrl = routes.rewriteUrl || {};
_routes.rewriteParams = routes.rewriteParams || {};
+ // FIXME вообще конечно лучше бы route был массивом, потому что нам важен порядок рутов... пока не трогаем )
var rawRoutes = routes.route || {};
var compiledRoutes = [];
+ var compiledRoutesHash = {};
for (var route in rawRoutes) {
+ var page = rawRoutes[route];
var compiled = ns.router.compile(route);
- compiled.page = rawRoutes[route];
+ compiled.page = page;
compiledRoutes.push(compiled);
+ compiledRoutesHash[page] = compiledRoutesHash[page] || [];
+ compiledRoutesHash[page].push(compiled);
}
_routes.route = compiledRoutes;
+ _routes.routeHash = compiledRoutesHash;
ns.router._routes = _routes;
+
+ // Типы нужны при генерации урла.
+ ns.router._regexps = {};
+ for (var id in ns.router.regexps) {
+ ns.router._regexps[id] = new RegExp( ns.router.regexps[id] );
+ }
};
/**
- * Compile route.
- * @param {String} route
- * @return {{ regexp: RegExp, tokens: Array.<string> }}
-*/
-ns.router.compile = function(route) {
- // Отрезаем последний слэш, он ниже добавится как необязательный.
- var regexp = route.replace(/\/$/, '');
-
- var tokens = [];
- var defaults = {};
-
- // Заменяем {name} на кусок регэкспа соответствующего типу токена name.
- // Матч на слеш нужен, чтобы сделать слеш опциональным.
- regexp = regexp.replace(/(\/?){(.*?)}/g, function(_, slash, token) {
- var tokenParts = token.split(':');
- slash = slash || '';
-
- var type = tokenParts[1] || 'id';
- var rx_part = ns.router.regexps[type];
- if (!rx_part) {
- throw new Error("[ns.router] Can't find regexp for '" + type +"'!");
+ * Generate url.
+ * @param {string} url Relative url.
+ * @return {String} Valid url that takes into consideration baseDir.
+ */
+ns.router.url = function(url) {
+ return (ns.router.baseDir + url) || '/';
+};
+
+ns.router.generateUrl = function(id, params) {
+ var url;
+ var routes = ns.router._routes.routeHash[id];
+ params = params || {};
+
+ if (!routes || !routes.length) {
+ throw new Error("[ns.router] Could not find route with id '" + id + "'!");
+ }
+
+ for (var i = 0; i < routes.length; i++) {
+ url = ns.router._generateUrl(routes[i], params);
+ if (url) {
+ break;
}
+ }
- var tokenName = tokenParts[0];
- var equalSignIndex = tokenName.indexOf('=');
+ if (url === null) {
+ throw new Error("[ns.router] Could not generate url for layout id '" + id + "'!");
+ }
+
+ return ns.router.url(url);
+};
- if (equalSignIndex > 0) {
- var tokenDefault = tokenName.substring(equalSignIndex + 1);
- tokenName = tokenName.substring(0, equalSignIndex);
+/**
+ * @param {Object} def Url definition.
+ * @param {Object} params Url generation params.
+ * @return {?string} Generated url.
+ */
+ns.router._generateUrl = function(def, params) {
+ var url;
+ var part;
+ var pvalue;
+ var result = [];
+ var query = no.extend({}, params);
+ var rewrites = ns.router._routes.rewriteUrl;
+
+ for (var i = 0; i < def.parts.length; i++) {
+ part = def.parts[i];
+ if (!part.name) {
+ // Добавляем статический кусок урла как есть.
+ result.push(part.default_value);
+ } else {
+ pvalue = params[part.name] || part.default_value;
- tokens.push(tokenName);
- defaults[tokenName] = tokenDefault;
- if (slash) {
- return '(?:' + slash + '(' + rx_part + '))?';
+ // Обязательный параметр должен быть указан.
+ if (!part.is_optional && !pvalue) {
+ return null;
}
- else {
- return '(' + rx_part + ')?';
+
+ // Опциональный параметр не должен попасть в урл, если он не указан явно в params.
+ if (part.is_optional && !(part.name in params)) {
+ continue;
}
- } else {
- // Запоминаем имя токена, оно нужно при парсинге урлов.
- tokens.push(tokenName);
- return slash + '(' + rx_part + ')';
+ // Проверка типа.
+ if (!ns.router._regexps[part.type].test(pvalue)) {
+ return null;
+ }
+
+ result.push(pvalue);
+ delete query[part.name];
}
- });
- // Добавляем "якоря" ^ и $;
- // Плюс матчим необязательный query-string в конце урла, вида ?param1=value1&param2=value2...
+ }
+
+ url = result.join('/');
+ url = (url) ? ('/' + url) : '';
+
+ // Разворачиваем rewrite правила, чтобы получить красивый урл до rewrite-ов.
+ var rewrote = true;
+ while (rewrote) {
+ rewrote = false;
+ for (var srcUrl in rewrites) {
+ if (url === rewrites[srcUrl]) {
+ url = srcUrl;
+ rewrote = true;
+ }
+ }
+ }
+
+ // Дописываем query string.
+ var queryString = $.param(query);
+ return (queryString) ? (url + '?' + queryString) : url;
+};
+
+/**
+ * Compile route.
+ * @param {String} route
+ * @return {{ regexp: RegExp, tokens: Array.<string> }}
+*/
+ns.router.compile = function(route) {
+ // Удаляем слеши в начале и в конце урла.
+ route = route
+ .replace(/^\//, '')
+ .replace(/\/$/, '');
+
+ var parts = route.split('/');
+ var params = parts.map(ns.router._parseParam);
+ var pregexps = params.map(ns.router._generateParamRegexp);
+ var regexp = pregexps.join('');
+
+ // Добавляем "якоря" ^ и $;
+ // Плюс матчим необязательный query-string в конце урла, вида ?param1=value1&param2=value2...
regexp = '^' + regexp + '\/?(?:\\?(.*))?$';
return {
regexp: new RegExp(regexp),
- tokens: tokens,
- defaults: defaults
+ params: params.filter(function(p) { return !!p.name; }), // оставляем только настоящие параметры, а не статические части урла
+ parts: params // для генерации урла нужны все параметры
};
};
+ns.router._parseParam = function(param) {
+ var param_extract;
+ var type_parts;
+ var default_parts;
+ var param_type;
+ var param_default;
+ var param_is_optional;
+
+ // Параметр (указывается в фигурных скобках)
+ param_extract = /{([^}]+)}/.exec(param);
+ if (param_extract) {
+ // Самый сложный вариант: {name=default:type}
+ param = param_extract[1];
+
+ // parameter type (defaults to id)
+ type_parts = param.split(':');
+ param_type = type_parts[1] || 'id';
+
+ // parameter default value and if parameter is optional
+ param = type_parts[0];
+ default_parts = param.split('=');
+ param_default = default_parts[1];
+ param_is_optional = (default_parts.length > 1);
+
+ // section parsed
+ param = default_parts[0];
+ return {
+ name: param,
+ type: param_type,
+ default_value: param_default,
+ is_optional: param_is_optional
+ };
+ } else {
+ // статический кусок урла
+ return {
+ default_value: param
+ };
+ }
+};
+
+ns.router._generateParamRegexp = function(p) {
+ var re;
+ var regexps = ns.router.regexps;
+
+ // static text
+ if (p.default_value && !p.name) {
+ return '/' + p.default_value;
+ }
+
+ // validate parameter type is known (if specified)
+ if (p.type && !(p.type in regexps)) {
+ throw new Error("[ns.router] Could not find regexp for '" + p.type + "'!");
+ }
+
+ re = regexps[p.type];
+ re = '/(' + re + ')';
+
+ if (p.is_optional) {
+ re = '(?:' + re + ')?';
+ }
+
+ return re;
+};
+
/**
* Базовая часть урла, относительно которой строятся урлы. Без слэша на конце.
* @type {String}
View
2  test/index.html
@@ -76,6 +76,8 @@
<script src="spec/ns.page.js"></script>
<script src="spec/ns.request.js"></script>
<script src="spec/ns.router.js"></script>
+ <script src="spec/ns.router2.js"></script>
+ <script src="spec/ns.router.generateUrl.js"></script>
<script src="spec/ns.updater.js"></script>
<script src="spec/ns.view.js"></script>
<script src="spec/ns.view.bind-events.js"></script>
View
252 test/spec/ns.router.generateUrl.js
@@ -0,0 +1,252 @@
+describe('generate url', function() {
+ afterEach(function() {
+ ns.router.baseDir = '';
+ ns.router.undefine();
+ });
+
+ describe('error thrown', function() {
+ beforeEach(function() {
+ ns.router.routes = {};
+ ns.router.init();
+ });
+
+ afterEach(function() {
+ delete ns.router.baseDir;
+ ns.router.undefine();
+ });
+
+ it('if page name is unknown', function() {
+ expect(function() { ns.router.generateUrl('new-page'); }).to.throwError()
+ });
+ });
+
+ describe('baseDir', function() {
+ beforeEach(function() {
+ ns.router.routes = {
+ route: {
+ '/index': 'index',
+ '/': 'root'
+ }
+ };
+ ns.router.init();
+ });
+
+ afterEach(function() {
+ ns.router.baseDir = '';
+ ns.router.undefine();
+ });
+
+ it('baseDir not specified', function() {
+ expect( ns.router.generateUrl('root') ).to.be('/');
+ expect( ns.router.generateUrl('index') ).to.be('/index');
+ });
+
+ it('baseDir specified', function() {
+ ns.router.baseDir = '/the-base'
+ expect( ns.router.generateUrl('root') ).to.be('/the-base');
+ expect( ns.router.generateUrl('index') ).to.be('/the-base/index');
+ });
+ });
+
+ describe('optional parameter', function() {
+ beforeEach(function() {
+ ns.router.routes = {
+ route: {
+ '/folder/{name=inbox}': 'folder',
+ '/{context=}/alert': 'alert-somewhere',
+ '/folder/{name=inbox}/file': 'folder-file'
+ }
+ };
+ ns.router.init();
+ });
+
+ afterEach(function() {
+ ns.router.baseDir = '';
+ ns.router.undefine();
+ });
+
+ it('not specified', function() {
+ expect( ns.router.generateUrl('folder') ).to.be('/folder');
+ expect( ns.router.generateUrl('folder', {}) ).to.be('/folder');
+ expect( ns.router.generateUrl('folder', { id: 5 }) ).to.be('/folder?id=5');
+ });
+
+ it('tail optional parameter', function() {
+ expect( ns.router.generateUrl('folder', { name: 'favorites' }) ).to.be('/folder/favorites');
+ expect( ns.router.generateUrl('folder', { name: 'inbox' }) ).to.be('/folder/inbox');
+ });
+
+ it('head optional parameter', function() {
+ expect( ns.router.generateUrl('alert-somewhere') ).to.be('/alert');
+ expect( ns.router.generateUrl('alert-somewhere', { context: 'inbox' }) ).to.be('/inbox/alert');
+ });
+
+ it('middle optional parameter', function() {
+ expect( ns.router.generateUrl('folder-file') ).to.be('/folder/file');
+ expect( ns.router.generateUrl('folder-file', { name: 'inbox' }) ).to.be('/folder/inbox/file');
+ expect( ns.router.generateUrl('folder-file', { name: 'favorites' }) ).to.be('/folder/favorites/file');
+ });
+ });
+
+ describe('mandatory parameter', function() {
+ beforeEach(function() {
+ ns.router.routes = {
+ route: {
+ '/folder/{name}': 'folder',
+ '/folder/{name}/{id:int}': 'file'
+ }
+ };
+ ns.router.init();
+ });
+
+ afterEach(function() {
+ ns.router.baseDir = '';
+ ns.router.undefine();
+ });
+
+ it('not specified', function() {
+ expect(function() { ns.router.generateUrl('folder') }).to.throwError();
+ expect(function() { ns.router.generateUrl('file', { name: 'inbox' }) }).to.throwError();
+ });
+ });
+
+ describe('type validation', function() {
+ beforeEach(function() {
+ ns.router.routes = {
+ route: {
+ '/{name}': 'folder',
+ '/{name}/{file:int}': 'file'
+ }
+ };
+ ns.router.init();
+ });
+
+ afterEach(function() {
+ ns.router.baseDir = '';
+ ns.router.undefine();
+ });
+
+ it ('default type is id', function() {
+ expect(function() { ns.router.generateUrl('folder', { name: 'abc' }); }).not.to.throwError();
+ expect(function() { ns.router.generateUrl('folder', { name: '1' }); }).to.throwError();
+ });
+
+ it ('type specified directly', function() {
+ expect(function() { ns.router.generateUrl('file', { name: 'abc', file: 7 }); }).not.to.throwError();
+ expect(function() { ns.router.generateUrl('file', { name: 'abc', file: 'myfile.txt' }); }).to.throwError();
+ });
+ });
+
+ describe('same layout with various params', function() {
+
+ beforeEach(function() {
+ ns.router.routes = {
+ route: {
+ '/compose/{oper:id}/{mid:int}': 'compose',
+ '/compose/{mid:int}': 'compose',
+ '/compose': 'compose'
+ }
+ };
+ ns.router.init();
+ });
+
+ var TESTS = [
+ {
+ layout: 'compose',
+ params: {},
+ url: '/compose'
+ },
+ {
+ layout: 'compose',
+ params: {mid: '1'},
+ url: '/compose/1'
+ },
+ {
+ layout: 'compose',
+ params: {mid: '2', oper: 'reply'},
+ url: '/compose/reply/2'
+ }
+ ];
+
+ TESTS.forEach(function(test) {
+
+ it('should generate "' + test.url +'" for "' + test.layout + '" ' + JSON.stringify(test.params), function() {
+ expect(
+ ns.router.generateUrl(test.layout, test.params)
+ ).to.be(test.url)
+ });
+
+ });
+
+ });
+
+ describe('reverse rewrites', function() {
+ beforeEach(function() {
+ ns.router.routes = {
+ rewriteUrl: {
+ '/': '/inbox',
+ '/inbox': '/folder/inbox',
+ '/shortcut': '/page/1'
+ },
+ route: {
+ '/page/{id:int}': 'page',
+ '/folder/{name}': 'folder'
+ }
+ };
+ ns.router.init();
+ });
+
+ afterEach(function() {
+ ns.router.baseDir = '';
+ ns.router.undefine();
+ });
+
+ it('no matching rewrites', function() {
+ expect(ns.router.generateUrl('page', { id: 2 })).to.be.eql('/page/2');
+ });
+
+ it('1 matching rewrite', function() {
+ expect(ns.router.generateUrl('page', { id: 1 })).to.be.eql('/shortcut');
+ });
+
+ it('multiple matching rewrites', function() {
+ expect(ns.router.generateUrl('folder', { name: 'inbox' })).to.be.eql('/');
+ });
+ });
+
+ describe('generate url with query', function() {
+ beforeEach(function() {
+ ns.router.routes = {
+ rewriteUrl: {
+ '/shortcut': '/page/1'
+ },
+ route: {
+ '/page/{id:int}': 'page'
+ }
+ };
+ ns.router.init();
+ });
+
+ afterEach(function() {
+ ns.router.baseDir = '';
+ ns.router.undefine();
+ });
+
+ it('empty query', function() {
+ expect(ns.router.generateUrl('page', { id: 2 })).to.be.eql('/page/2');
+ });
+
+ it('1 parameter goes into query', function() {
+ expect(ns.router.generateUrl('page', { id: 2, page: 7 })).to.be.eql('/page/2?page=7');
+ });
+
+ it('more than 1 parameter goes into query', function() {
+ expect(ns.router.generateUrl('page', { id: 2, page: 7, nc: '0123' })).to.be.eql('/page/2?page=7&nc=0123');
+ });
+
+ it('reverse rewrite and query', function() {
+ expect(ns.router.generateUrl('page', { id: 1, page: 7, nc: '0123' })).to.be.eql('/shortcut?page=7&nc=0123');
+ });
+ });
+
+});
View
81 test/spec/ns.router2.js
@@ -0,0 +1,81 @@
+function extractRegExp(r) {
+ return {
+ source: r.source,
+ global: r.global,
+ multiline: r.multiline,
+ ignoreCase: r.ignoreCase
+ };
+};
+
+function compareRegExp(a, b) {
+ expect(extractRegExp(a)).to.be.eql(extractRegExp(b));
+}
+
+// ----------------------------------------------------------------------------------------------------------------- //
+
+describe('router: new route parsing method', function() {
+
+ describe('parse parameters', function() {
+ var _tests = {
+ 'static': { default_value: 'static' },
+ '{param}': { name: 'param', type: undefined, default_value: undefined, is_optional: false },
+ '{param=}': { name: 'param', type: undefined, default_value: undefined, is_optional: true },
+ '{param:int}': { name: 'param', type: 'int', default_value: undefined, is_optional: false },
+ '{param=:int}': { name: 'param', type: 'int', default_value: undefined, is_optional: true },
+ '{param=value}': { name: 'param', type: undefined, default_value: 'value', is_optional: true },
+ '{param=value:int}': { name: 'param', type: 'int', default_value: 'value', is_optional: true }
+ };
+
+ for (var test in _tests) {
+ it(test, function() {
+ expect(ns.router._parseParam(test)).to.be.eql(_tests[test]);
+ });
+ }
+ });
+
+ describe('generate parameter regexp', function() {
+ var _tests = {
+ 'static': '/static',
+ '{param}': '/(.+)',
+ '{param=}': '(?:/(.+))?',
+ '{param:int}': '/([0-9]+)',
+ '{param=:int}': '(?:/([0-9]+))?',
+ '{param=value}': '/([0-9]+)',
+ '{param=value:int}': '(?:/([0-9]+))?'
+ };
+
+ for (var test in _tests) {
+ it(test, function() {
+ expect( ns.router._generateParamRegexp( ns.router._parseParam(test) )).to.be(_tests[test] );
+ });
+ }
+ });
+
+ describe('complex url 1', function() {
+ it('/{context=contest}/{contest-id:int}/users/{author-login}/album/', function() {
+ var route = '/{context=contest}/{contest-id:int}/users/{author-login}/album/';
+ var regexp = ns.router.compile(route).regexp;
+ compareRegExp( regexp, new RegExp('^(?:/([A-Za-z_][A-Za-z0-9_-]*))?/([0-9]+)/users/([A-Za-z_][A-Za-z0-9_-]*)/album/?(?:\\?(.*))?$') );
+ });
+ });
+
+ describe('complex url 2', function() {
+ it('/{context=}/users/{login}/view/{id:int}', function() {
+ var route = '/{context=}/users/{login}/view/{id:int}';
+ var regexp = ns.router.compile(route).regexp;
+ compareRegExp( regexp, new RegExp('^(?:/([A-Za-z_][A-Za-z0-9_-]*))?/users/([A-Za-z_][A-Za-z0-9_-]*)/view/([0-9]+)/?(?:\\?(.*))?$') );
+ });
+ });
+
+ describe('complex url 3', function() {
+ it('/message/{id=:int}', function() {
+ var route = '/message/{id=:int}';
+ var regexp = ns.router.compile(route).regexp;
+ compareRegExp( regexp, new RegExp('^/message(?:/([0-9]+))?/?(?:\\?(.*))?$') ); // TODO проверить, что не получится 2 слеша на конце
+ });
+ });
+
+ describe('double end slash', function() {
+ it('is not valid');
+ });
+});
Something went wrong with that request. Please try again.