Skip to content

Commit

Permalink
Parameter filter #204
Browse files Browse the repository at this point in the history
  • Loading branch information
chestozo committed Mar 23, 2014
1 parent c623cba commit 53b2eb6
Show file tree
Hide file tree
Showing 3 changed files with 180 additions and 60 deletions.
61 changes: 43 additions & 18 deletions src/ns.router.js
Expand Up @@ -110,6 +110,12 @@ ns.router = function(url) {
ns.router.init = function() {
var routes = ns.router.routes;

// Типы параметров (нужны при валидации и при генерации урлов).
ns.router._regexps = {};
for (var id in ns.router.regexps) {
ns.router._regexps[id] = new RegExp( ns.router.regexps[id] );
}

var _routes = {};
_routes.redirect = routes.redirect || {};
_routes.rewriteUrl = routes.rewriteUrl || {};
Expand All @@ -132,12 +138,6 @@ ns.router.init = function() {
_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] );
}
};

/**
Expand Down Expand Up @@ -208,7 +208,7 @@ ns.router._generateUrl = function(def, params) {
}

// Проверка типа.
if (!ns.router._regexps[param.type].test(pvalue)) {
if (!ns.router._isParamValid(pvalue, param.type)) {
return null;
}

Expand Down Expand Up @@ -325,6 +325,7 @@ ns.router._parseSection = function(rawSection) {
* Парсит декларацию параметра (то, что внутри фигурных скобок.
* Пример:
* name=default:type
* name==filter:type
*/
ns.router._parseParam = function(param) {
var type_parts;
Expand All @@ -333,18 +334,30 @@ ns.router._parseParam = function(param) {
var param_default;
var param_is_optional;

// parameter type (defaults to id)
// parameter type (defaults to id).
type_parts = param.split(':');
param_type = type_parts[1] || 'id';

// parameter default value and if parameter is optional
// parameter default value and param_is_optional flag.
param = type_parts[0];
default_parts = param.split('=');
param_default = default_parts[1];
param_is_optional = (default_parts.length > 1);

if (param.indexOf('==') >= 0) {
default_parts = param.split('==');
param_is_optional = false;
param = default_parts[0];
param_default = default_parts[1];

ns.assert(param_default, 'ns.router', "Parameter '%s' value must be specified", param);
ns.assert(ns.router._isParamValid(param_default, param_type), 'ns.router', "Wrong value for '%s' parameter", param);

} else {
default_parts = param.split('=');
param_is_optional = (default_parts.length > 1);
param = default_parts[0];
param_default = default_parts[1];
}

// section parsed
param = default_parts[0];
return {
name: param,
type: param_type,
Expand Down Expand Up @@ -380,19 +393,31 @@ ns.router._generateParamRegexp = function(p) {

// validate parameter type is known (if specified)
if (p.type) {
ns.assert((p.type in regexps), 'ns.router', "Could not find regexp for '%s'!", p.type);
ns.assert((p.type in regexps), 'ns.router', "Could not find regexp for type '%s'!", p.type);
}

re = regexps[p.type];
re = '(' + re + ')';
// parameter with filter (param==value)
if (!p.is_optional && p.default_value) {
re = '(' + p.default_value + ')';

if (p.is_optional) {
re = '(?:' + re + ')?';
} else {
re = regexps[p.type];
re = '(' + re + ')';

if (p.is_optional) {
re = '(?:' + re + ')?';
}
}

return re;
};

ns.router._isParamValid = function(pvalue, ptype) {
var _regexp = ns.router._regexps[ptype];
ns.assert(_regexp, 'ns.router', "Could not find regexp for type '%s'!", ptype);
return _regexp.test(pvalue);
};

/**
* Базовая часть урла, относительно которой строятся урлы. Без слэша на конце.
* @type {String}
Expand Down
25 changes: 25 additions & 0 deletions test/spec/ns.router.js
Expand Up @@ -116,6 +116,31 @@ describe('ns.router', function() {
test_route('/messages//45', 'not-found', {}, '/messages//45: MUST FAIL');
});

describe('filter value', function() {

beforeEach(function() {
ns.router.regexps['context'] = 'search|tag|top';
ns.router.routes = {
route: {
'/{context==search:context}/{query}/image/{id:int}': 'view',
'/{context==tag:context}/{tag}/image/{id:int}': 'view',
'/{context=:context}/image/{id:int}': 'view'
}
};
ns.router.init();
});

var test_route = function(url, page, params, test_name) {
it(test_name || url, function() {
expect(ns.router(url)).to.be.eql({ page: page, params: params });
});
};

test_route('/search/airport/image/1', 'view', { context: 'search', query: 'airport', id: 1 });
test_route('/tag/airport/image/2', 'view', { context: 'tag', tag: 'airport', id: 2 });
test_route('/top/image/3', 'view', { context: 'top', id: 3 });
});

describe('baseDir: routing', function() {
beforeEach(function() {
ns.router.baseDir = '/ver2';
Expand Down
154 changes: 112 additions & 42 deletions test/spec/ns.router2.js
Expand Up @@ -15,20 +15,39 @@ function compareRegExp(a, b) {

describe('router: new route parsing method', function() {

beforeEach(function() {
ns.router.init();
});

afterEach(function() {
delete ns.router._routes;
delete ns.router.routes;
});

describe('parse parameter', function() {
var _tests = {
'param': { name: 'param', type: 'id', default_value: undefined, is_optional: false },
'param=': { name: 'param', type: 'id', default_value: '', is_optional: true },
'param:int': { name: 'param', type: 'int', default_value: undefined, is_optional: false },
'param=:int': { name: 'param', type: 'int', default_value: '', is_optional: true },
'param=value': { name: 'param', type: 'id', default_value: 'value', is_optional: true },
'param=value:int': { name: 'param', type: 'int', default_value: 'value', is_optional: true }
'param': { name: 'param', type: 'id', default_value: undefined, is_optional: false },
'param=': { name: 'param', type: 'id', default_value: '', is_optional: true },
'param:int': { name: 'param', type: 'int', default_value: undefined, is_optional: false },
'param=:int': { name: 'param', type: 'int', default_value: '', is_optional: true },
'param=value': { name: 'param', type: 'id', default_value: 'value', is_optional: true },
'param=value:int': { name: 'param', type: 'int', default_value: 'value', is_optional: true },
'param==': { throw: /^\[ns\.router\] Parameter 'param' value must be specified$/ },
'param==:int': { throw: /^\[ns\.router\] Parameter 'param' value must be specified$/ },
'param==value': { name: 'param', type: 'id', default_value: 'value', is_optional: false },
'param==value:int': { throw: /^\[ns\.router\] Wrong value for 'param' parameter$/ },
'param==123:int': { name: 'param', type: 'int', default_value: '123', is_optional: false }
};

for (var test in _tests) {
(function(test) {
it(test, function() {
expect(ns.router._parseParam(test)).to.be.eql(_tests[test]);
var result = _tests[test];
if (result.throw) {
expect(function() { ns.router._parseParam(test); }).to.throwError(result.throw);
} else {
expect(ns.router._parseParam(test)).to.be.eql(result);
}
});
})(test);
}
Expand All @@ -39,37 +58,45 @@ describe('router: new route parsing method', function() {
// 'int': '[0-9]+'

var _tests = {
'param': '([A-Za-z_][A-Za-z0-9_-]*)',
'param=': '(?:([A-Za-z_][A-Za-z0-9_-]*))?',
'param:int': '([0-9]+)',
'param=:int': '(?:([0-9]+))?',
'param=value': '(?:([A-Za-z_][A-Za-z0-9_-]*))?',
'param=value:int': '(?:([0-9]+))?'
'param': '([A-Za-z_][A-Za-z0-9_-]*)',
'param=': '(?:([A-Za-z_][A-Za-z0-9_-]*))?',
'param:int': '([0-9]+)',
'param=:int': '(?:([0-9]+))?',
'param=value': '(?:([A-Za-z_][A-Za-z0-9_-]*))?',
'param=value:int': '(?:([0-9]+))?',
'param:new-type': { throw: /^\[ns\.router\] Could not find regexp for type 'new\-type'!$/ },
'param==value': '(value)',
'param==123:int': '(123)',
'param==value:int': { throw: /^\[ns\.router\] Wrong value for 'param' parameter$/ },
'param==value:new-type': { throw: /^\[ns\.router\] Could not find regexp for type 'new\-type'!$/ },
};

for (var test in _tests) {
(function(test) {
it(test, function() {
expect( ns.router._generateParamRegexp( ns.router._parseParam(test) )).to.be(_tests[test] );
var result = _tests[test];
if (result.throw) {
expect(function() { ns.router._generateParamRegexp(ns.router._parseParam(test)); }).to.throwError(result.throw);
} else {
expect(ns.router._generateParamRegexp(ns.router._parseParam(test))).to.be.eql(result);
}
});
})(test);
}

it('should throw error for unknown parameter type', function() {
expect(function() { ns.router._generateParamRegexp(ns.router._parseParam('param:new-type')); }).to.throwError(/\[ns\.router\] Could not find regexp for 'new\-type'!/);
});
});

describe('parse section', function() {
var _tests = {
'static': { is_optional: false, items: [ { default_value: 'static' } ] },
'{param}': { is_optional: false, items: [ { name: 'param', type: 'id', default_value: undefined, is_optional: false } ] },
'{param=}': { is_optional: true, items: [ { name: 'param', type: 'id', default_value: '', is_optional: true } ] },
'{param:int}': { is_optional: false, items: [ { name: 'param', type: 'int', default_value: undefined, is_optional: false } ] },
'{param=:int}': { is_optional: true, items: [ { name: 'param', type: 'int', default_value: '', is_optional: true } ] },
'{param=value}': { is_optional: true, items: [ { name: 'param', type: 'id', default_value: 'value', is_optional: true } ] },
'{param=value:int}': { is_optional: true, items: [ { name: 'param', type: 'int', default_value: 'value', is_optional: true } ] },
'prefix-{param:int}': { is_optional: false, items: [ { default_value: 'prefix-' }, { name: 'param', type: 'int', default_value: undefined, is_optional: false } ] },
'static': { is_optional: false, items: [ { default_value: 'static' } ] },
'{param}': { is_optional: false, items: [ { name: 'param', type: 'id', default_value: undefined, is_optional: false } ] },
'{param=}': { is_optional: true, items: [ { name: 'param', type: 'id', default_value: '', is_optional: true } ] },
'{param:int}': { is_optional: false, items: [ { name: 'param', type: 'int', default_value: undefined, is_optional: false } ] },
'{param=:int}': { is_optional: true, items: [ { name: 'param', type: 'int', default_value: '', is_optional: true } ] },
'{param=value}': { is_optional: true, items: [ { name: 'param', type: 'id', default_value: 'value', is_optional: true } ] },
'{param=value:int}': { is_optional: true, items: [ { name: 'param', type: 'int', default_value: 'value', is_optional: true } ] },
'{param==value:int}': { throw: /^\[ns\.router\] Wrong value for 'param' parameter$/ },
'{param==123:int}': { is_optional: false, items: [ { name: 'param', type: 'int', default_value: '123', is_optional: false } ] },
'prefix-{param:int}': { is_optional: false, items: [ { default_value: 'prefix-' }, { name: 'param', type: 'int', default_value: undefined, is_optional: false } ] },
'prefix-{part1:int}{part2=}': {
is_optional: false,
items: [
Expand All @@ -78,26 +105,30 @@ describe('router: new route parsing method', function() {
{ name: 'part2', type: 'id', default_value: '', is_optional: true },
]
},
'some{thing': { throw: /^\[ns\.router\] could not parse parameter in url section: some{thing$/ }
};

for (var test in _tests) {
(function(test) {
it(test, function() {
expect(ns.router._parseSection(test)).to.be.eql(_tests[test]);
var result = _tests[test];
if (result.throw) {
expect(function() { ns.router._parseSection(test); }).to.throwError(result);
} else {
expect(ns.router._parseSection(test)).to.be.eql(result);
}
});
})(test);
}

it('should throw error if there is not closing braket', function() {
expect(function() { ns.router._parseSection('some{thing'); }).to.throwError(/\[ns\.router\] could not parse parameter in url section: some{thing/);
});
});

describe('generate section regexp', function() {
var _tests = {
'{param}': '/([A-Za-z_][A-Za-z0-9_-]*)',
'{param:int}': '/([0-9]+)',
'{param=value:int}': '(?:/(?!/)(?:([0-9]+))?)?'
'{param}': '/([A-Za-z_][A-Za-z0-9_-]*)',
'{param:int}': '/([0-9]+)',
'{param=value:int}': '(?:/(?!/)(?:([0-9]+))?)?',
'{param==value}': '/(value)',
'{param==123:int}': '/(123)'

This comment has been minimized.

Copy link
@chestozo

chestozo Mar 23, 2014

Member

Вот тут появляется возможность инжектить regexp, кстати, что не очень хорошо, но можно считать фичей )

};

for (var test in _tests) {
Expand Down Expand Up @@ -137,24 +168,63 @@ describe('router: new route parsing method', function() {
beforeEach(function() {
ns.router.routes = {
route: {
'/{context==search}/{query}/image/{id:int}': 'view',
'/{context==tag}/{tag}/image/{id:int}': 'view',
'/{context==top}/image/{id:int}': 'view',
'/test/{id}': 'test'
}
};
ns.router.init();
});

afterEach(function() {
ns.router.baseDir = '';
ns.router.undefine();
});

it('should throw error for unknown route', function() {
expect(function() { ns.router.generateUrl('test-no-such-route', {}); }).to.throwError(/\[ns\.router\] Could not find route with id 'test-no-such-route'!/);
});

it('should throw error when not enough params', function() {
expect(function() { ns.router.generateUrl('test', {}); }).to.throwError(/\[ns\.router\] Could not generate url for layout id 'test'!/);
});
var _tests = [
{
name: 'should throw error for unknown route',
id: 'test-no-such-route',
params: {},
result: { throw: /^\[ns\.router\] Could not find route with id 'test-no-such-route'!$/ }
},
{
name: 'should throw error when not enough params',
id: 'test',
params: {},
result: { throw: /^\[ns\.router\] Could not generate url for layout id 'test'!$/ }
},
{
name: 'generate route for parameter with filter 1',
id: 'view',
params: { context: 'top', id: 1 },
result: '/top/image/1'
},
{
name: 'generate route for parameter with filter 2',
id: 'view',
params: { context: 'search', query: 'airport', id: 2 },
result: '/search/airport/image/2'
},
{
name: 'generate route for parameter with filter 3',
id: 'view',
params: { context: 'tag', tag: 'summer', id: 3 },
result: '/tag/summer/image/3'
}
];

for (var i = 0; i < _tests.length; i++) {
(function(test) {
it(test.name, function() {
if (test.result.throw) {
expect(function() { ns.router.generateUrl(test.id, test.params); }).to.throwError(test.result.throw);
} else {
expect(ns.router.generateUrl(test.id, test.params)).to.be.eql(test.result);
}
});
})(_tests[i]);
}
});

// describe('ns.router.compare()', function() {
Expand Down

0 comments on commit 53b2eb6

Please sign in to comment.