From 40b04c8816d5e5c93bd5251e0a7a2daa46256bc5 Mon Sep 17 00:00:00 2001 From: Ruslan Kerimov Date: Sun, 12 Oct 2014 15:59:12 +0400 Subject: [PATCH] Fix #18, fix and add some tests --- Makefile | 4 +- dist/susanin.js | 139 +++++++++++----------- dist/susanin.min.js | 2 +- karma.conf.js | 2 +- lib/route.js | 123 ++++++++++--------- lib/router.js | 16 ++- test/{lib/browser.js => browser/setup.js} | 0 test/data_matching.js | 73 ++++++++++++ test/{lib/nodejs.js => nodejs/setup.js} | 2 +- test/querystring.js | 6 + test/route.getData.js | 6 +- test/route.getName.js | 5 +- test/route.match.js | 17 +++ test/router.addRoute.js | 2 +- test/router.find.js | 4 +- test/router.getRouteByName.js | 4 +- 16 files changed, 249 insertions(+), 156 deletions(-) rename test/{lib/browser.js => browser/setup.js} (100%) create mode 100644 test/data_matching.js rename test/{lib/nodejs.js => nodejs/setup.js} (66%) diff --git a/Makefile b/Makefile index d76e478..dce6246 100644 --- a/Makefile +++ b/Makefile @@ -25,7 +25,7 @@ unittests: nodejsunittests browsersunittests .PHONY: unittests_in_nodejs nodejsunittests: $(MOCHA) - $(MOCHA) -u bdd -R spec -r chai $(PRJ_DIR)test/lib/nodejs.js $(PRJ_DIR)test/*.js + $(MOCHA) -u bdd -R spec -r chai $(PRJ_DIR)test/nodejs $(PRJ_DIR)test .PHONY: unittests_in_browsers browsersunittests: $(KARMA) build @@ -41,7 +41,7 @@ jscs: $(JSCS) .PHONY: coverage coverage: $(ISTANBUL) $(_MOCHA) - $(ISTANBUL) cover $(_MOCHA) -- -u exports $(PRJ_DIR)test + $(ISTANBUL) cover $(_MOCHA) -- -u bdd $(PRJ_DIR)test/nodejs $(PRJ_DIR)test $(JSHINT) $(MOCHA) $(_MOCHA) $(ISTANBUL) $(JSCS) $(BORSCHIK) $(KARMA): npm install diff --git a/dist/susanin.js b/dist/susanin.js index 83fbb6f..5f00b34 100644 --- a/dist/susanin.js +++ b/dist/susanin.js @@ -219,8 +219,6 @@ function Route(options) { options.conditions && typeof options.conditions === 'object' || (options.conditions = {}); options.defaults && typeof options.defaults === 'object' || (options.defaults = {}); - options.data && typeof options.data === 'object' || (options.data = {}); - typeof options.name === 'string' && (options.data.name = options.name); if (options.isTrailingSlashOptional !== false) { options.pattern += GROUP_OPENED_CHAR + PARAM_OPENED_CHAR + @@ -340,7 +338,7 @@ Route.prototype._parseParams = function(pattern, parts) { * @private */ Route.prototype._buildParseRegExp = function() { - this._paramsMap = []; + this._reqExpParamsMap = []; this._parseRegExpSource = '^' + this._buildParseRegExpParts(this._parts) + '$'; this._parseRegExp = new RegExp(this._parseRegExpSource); }; @@ -360,10 +358,10 @@ Route.prototype._buildParseRegExpParts = function(parts) { if (typeof part === 'string') { ret += escape(part); - } else if (part && part.what === 'param') { - this._paramsMap.push(part.name); + } else if (part.what === 'param') { + this._reqExpParamsMap.push(part.name); ret += '(' + this._buildParamValueRegExpSource(part.name) + ')'; - } else if (part && part.what === 'optional') { + } else { ret += '(?:' + this._buildParseRegExpParts(part.parts) + ')?'; } } @@ -397,6 +395,7 @@ Route.prototype._buildParamValueRegExpSource = function(paramName) { * @private */ Route.prototype._buildBuildFn = function() { + this._mainParamsMap = {}; this._buildFnSource = 'var h=({}).hasOwnProperty;return ' + this._buildBuildFnParts(this._parts) + ';'; /*jshint evil:true */ this._buildFn = new Function('p', this._buildFnSource); @@ -418,14 +417,15 @@ Route.prototype._buildBuildFnParts = function(parts) { if (typeof part === 'string') { ret += '+"' + escape(part) + '"' ; - } else if (part && part.what === 'param') { + } else if (part.what === 'param') { + this._mainParamsMap[part.name] = true; ret += '+(h.call(p,"' + escape(part.name) + '")?' + 'p["' + escape(part.name) + '"]:' + (has(defaults, part.name) ? '"' + escape(defaults[part.name]) + '"' : '""') + ')'; - } else if (part && part.what === 'optional') { + } else { ret += '+((false'; for (j = 0, sizeJ = part.dependOnParams.length; j < sizeJ; ++j) { @@ -447,11 +447,36 @@ Route.prototype._buildBuildFnParts = function(parts) { }; /** - * Matches object with route - * @param {Object|String} matchObject + * @param {Object|Function} data + * @returns {Boolean} + * @private + */ +Route.prototype._isDataMatched = function(data) { + var routeData = this._options.data, + key; + + if (typeof data === 'function') { + return Boolean(data(routeData)); + } else if (data && typeof data === 'object') { + for (key in data) { + if (has(data, key)) { + if ( ! routeData || typeof routeData !== 'object' || routeData[key] !== data[key]) { + return false; + } + } + } + } + + return true; +}; + +/** + * Matches path with route + * @param {String} path + * @param {Function|Object} [data] * @returns {Object|null} */ -Route.prototype.match = function(matchObject) { +Route.prototype.match = function(path, data) { var ret = null, paramName, matches, @@ -462,55 +487,39 @@ Route.prototype.match = function(matchObject) { filter = options.postMatch, defaults = options.defaults; - if (typeof matchObject === 'string') { - matchObject = { path : matchObject }; - } else if ( ! matchObject) { + if (typeof path !== 'string' || (data && ! this._isDataMatched(data))) { return ret; } - for (key in matchObject) { - if (has(matchObject, key) && key !== 'path') { - if (options.data[key] !== matchObject[key]) { - return ret; - } - } - } + matches = path.match(this._parseRegExp); - if (typeof matchObject.path === 'string') { - matches = matchObject.path.match(this._parseRegExp); - - if (matches) { - ret = {}; - - for (i = 1, size = matches.length; i < size; ++i) { - if (typeof matches[i] !== 'undefined' && /* for IE lt 9*/ matches[i] !== '') { - paramName = this._paramsMap[i - 1]; - if (paramName !== TRAILING_SLASH_PARAM_NAME) { - ret[paramName] = matches[i]; - } else if ( - matchObject.path.charAt(matchObject.path.length - 2) === TRAILING_SLASH_PARAM_VALUE - ) { - return null; - } + if (matches) { + ret = {}; + + for (i = 1, size = matches.length; i < size; ++i) { + if (typeof matches[i] !== 'undefined' && /* for IE lt 9*/ matches[i] !== '') { + paramName = this._reqExpParamsMap[i - 1]; + if (paramName !== TRAILING_SLASH_PARAM_NAME) { + ret[paramName] = matches[i]; + } else if (path.charAt(path.length - 2) === TRAILING_SLASH_PARAM_VALUE) { + return null; } } + } - queryParams = querystring.parse(ret[QUERY_STRING_PARAM_NAME]); - for (key in queryParams) { - if (has(queryParams, key) && ! has(ret, key)) { - ret[key] = queryParams[key]; - } + queryParams = querystring.parse(ret[QUERY_STRING_PARAM_NAME]); + for (key in queryParams) { + if (has(queryParams, key) && ! has(ret, key)) { + ret[key] = queryParams[key]; } - delete ret[QUERY_STRING_PARAM_NAME]; + } + delete ret[QUERY_STRING_PARAM_NAME]; - for (key in defaults) { - if (has(defaults, key) && ! has(ret, key)) { - ret[key] = defaults[key]; - } + for (key in defaults) { + if (has(defaults, key) && ! has(ret, key)) { + ret[key] = defaults[key]; } } - } else { - ret = {}; } if (ret && typeof filter === 'function') { @@ -533,8 +542,6 @@ Route.prototype.build = function(params) { queryParams = {}, queryString, key, - isMainParam, - i, size, filter = this._options.preBuild; if (typeof filter === 'function') { @@ -547,15 +554,7 @@ Route.prototype.build = function(params) { params[key] !== null && typeof params[key] !== 'undefined' ) { - isMainParam = false; - for (i = 0, size = this._paramsMap.length; i < size; ++i) { - if (this._paramsMap[i] === key) { - isMainParam = true; - break; - } - } - - if (isMainParam) { + if (this._mainParamsMap[key]) { newParams[key] = params[key]; } else { queryParams[key] = params[key]; @@ -579,10 +578,10 @@ Route.prototype.getData = function() { /** * Returns name of the route - * @returns {?String} + * @returns {*} */ Route.prototype.getName = function() { - return this._options.data.name; + return this._options.name; }; module.exports = Route; @@ -613,7 +612,7 @@ function Router() { /** * Add route - * @param {Object|String} options + * @param {RouteOptions} options * @returns {Route} */ Router.prototype.addRoute = function(options) { @@ -631,17 +630,16 @@ Router.prototype.addRoute = function(options) { /** * Returns all successfully matched routes - * @param {Object|String} matchObject - * @returns {Array|null} + * @returns {[ Route, Object ][]} */ -Router.prototype.find = function(matchObject) { +Router.prototype.find = function() { var ret = [], parsed, i, size, routes = this._routes; for (i = 0, size = routes.length; i < size; ++i) { - parsed = routes[i].match(matchObject); + parsed = routes[i].match.apply(routes[i], arguments); if (parsed !== null) { ret.push([ routes[i], parsed ]); } @@ -652,16 +650,15 @@ Router.prototype.find = function(matchObject) { /** * Returns first successfully matched route - * @param {Object|String} matchObject - * @returns {Array|null} + * @returns {[ Route, Object ]|null} */ -Router.prototype.findFirst = function(matchObject) { +Router.prototype.findFirst = function() { var parsed, i, size, routes = this._routes; for (i = 0, size = routes.length; i < size; ++i) { - parsed = routes[i].match(matchObject); + parsed = routes[i].match.apply(routes[i], arguments); if (parsed !== null) { return [ routes[i], parsed ]; } diff --git a/dist/susanin.min.js b/dist/susanin.min.js index e8df183..18aa688 100644 --- a/dist/susanin.min.js +++ b/dist/susanin.min.js @@ -1 +1 @@ -!function(t){function e(t){return{"./querystring":function(){var t={},e=Object.prototype.hasOwnProperty,r=Object.prototype.toString,n=function(t){return"[object Array]"===r.call(t)},o={decode:function(t){var e;try{e=decodeURIComponent(t.replace(/\+/g,"%20"))}catch(r){e=t}return e},parse:function(t,r,i){var a,s,p,u,f,h,l={};if("string"!=typeof t||""===t)return l;for(r||(r="&"),i||(i="="),a=t.split(r),f=0,h=a.length;h>f;++f)s=a[f].split(i),p="undefined"!=typeof s[1]?o.decode(s[1]):"",u=o.decode(s[0]),e.call(l,u)?n(l[u])?l[u].push(p):l[u]=[l[u],p]:l[u]=p;return l},stringify:function(t,r,n){var o,i,a,s,p,u,f="";if(!t)return f;r||(r="&"),n||(n="=");for(u in t)if(e.call(t,u))for(a=[].concat(t[u]),s=0,p=a.length;p>s;++s)i=typeof a[s],o="object"===i||"undefined"===i?"":encodeURIComponent(a[s]),f+=r+encodeURIComponent(u)+n+o;return f.substr(r.length)}};return t.exports=o,t.exports},"./route":function(){function t(e){if(!(this instanceof t))return new t(e);if("string"==typeof e&&(e={pattern:e}),!e||"object"!=typeof e)throw new Error("You must specify options");if("string"!=typeof e.pattern)throw new Error("You must specify the pattern of the route");this._options=e,e.conditions&&"object"==typeof e.conditions||(e.conditions={}),e.defaults&&"object"==typeof e.defaults||(e.defaults={}),e.data&&"object"==typeof e.data||(e.data={}),"string"==typeof e.name&&(e.data.name=e.name),e.isTrailingSlashOptional!==!1&&(e.pattern+=l+f+g+h+c,e.conditions[g]=b),e.pattern+=l+"?"+f+P+h+c,e.conditions[P]=".*",this._parts=this._parsePattern(e.pattern),this._buildParseRegExp(),this._buildBuildFn()}var r={},n=Object.prototype.hasOwnProperty,o=function(t,e){return n.call(t,e)},i=Object.prototype.toString,a=function(t){return"[object Array]"===i.call(t)},s=e("./querystring"),p=function(){var t=["/",".","*","+","?","|","(",")","[","]","{","}","\\"],e=new RegExp("(\\"+t.join("|\\")+")","g");return function(t){return t.replace(e,"\\$1")}}(),u=String(Math.random()).substr(2,5),f="<",h=">",l="(",c=")",d="[a-zA-Z_][\\w\\-]*",y="[\\w\\-\\.~]+",m=new RegExp("("+p(f)+d+p(h)+"|"+"[^"+p(f)+p(h)+"]+"+"|"+p(f)+"|"+p(h)+")","g"),g="ts_"+u,_="/",b=p("/"),P="qs_"+u;return t.prototype._parsePattern=function(t){for(var e,r,n,o=[],i="",a=0,s=0,p=!1,u=t.length;u>a;)if(e=t.charAt(a++),e===l)p?(++s,i+=e):(this._parseParams(i,o),i="",s=0,p=!0);else if(e===c)if(p)if(0===s){for(i={what:"optional",dependOnParams:[],parts:this._parsePattern(i)},o.push(i),r=0,n=i.parts.length;n>r;++r)i.parts[r]&&"param"===i.parts[r].what&&i.dependOnParams.push(i.parts[r].name);i="",p=!1}else--s,i+=e;else i+=e;else i+=e;return this._parseParams(i,o),o},t.prototype._parseParams=function(t,e){var r,n,o,i=t.match(m);if(i)for(r=0,n=i.length;n>r;++r)o=i[r],o.charAt(0)===f&&o.charAt(o.length-1)===h?e.push({what:"param",name:o.substr(1,o.length-2)}):e.push(o)},t.prototype._buildParseRegExp=function(){this._paramsMap=[],this._parseRegExpSource="^"+this._buildParseRegExpParts(this._parts)+"$",this._parseRegExp=new RegExp(this._parseRegExpSource)},t.prototype._buildParseRegExpParts=function(t){var e,r,n,o="";for(e=0,r=t.length;r>e;++e)n=t[e],"string"==typeof n?o+=p(n):n&&"param"===n.what?(this._paramsMap.push(n.name),o+="("+this._buildParamValueRegExpSource(n.name)+")"):n&&"optional"===n.what&&(o+="(?:"+this._buildParseRegExpParts(n.parts)+")?");return o},t.prototype._buildParamValueRegExpSource=function(t){var e,r=this._options.conditions[t];return e=r?a(r)?"(?:"+r.join("|")+")":r+"":y},t.prototype._buildBuildFn=function(){this._buildFnSource="var h=({}).hasOwnProperty;return "+this._buildBuildFnParts(this._parts)+";",this._buildFn=new Function("p",this._buildFnSource)},t.prototype._buildBuildFnParts=function(t){var e,r,n,i,a,s,u='""',f=this._options.defaults;for(e=0,r=t.length;r>e;++e)if(a=t[e],"string"==typeof a)u+='+"'+p(a)+'"';else if(a&&"param"===a.what)u+='+(h.call(p,"'+p(a.name)+'")?'+'p["'+p(a.name)+'"]:'+(o(f,a.name)?'"'+p(f[a.name])+'"':'""')+")";else if(a&&"optional"===a.what){for(u+="+((false",n=0,i=a.dependOnParams.length;i>n;++n)s=a.dependOnParams[n],u+='||(h.call(p,"'+p(s)+'")'+(o(f,s)?'&&p["'+p(s)+'"]!=="'+p(f[s])+'"':"")+")";u+=")?("+this._buildBuildFnParts(a.parts)+'):"")'}return u},t.prototype.match=function(t){var e,r,n,i,a,p,u=null,f=this._options,h=f.postMatch,l=f.defaults;if("string"==typeof t)t={path:t};else if(!t)return u;for(a in t)if(o(t,a)&&"path"!==a&&f.data[a]!==t[a])return u;if("string"==typeof t.path){if(r=t.path.match(this._parseRegExp)){for(u={},n=1,i=r.length;i>n;++n)if("undefined"!=typeof r[n]&&""!==r[n])if(e=this._paramsMap[n-1],e!==g)u[e]=r[n];else if(t.path.charAt(t.path.length-2)===_)return null;p=s.parse(u[P]);for(a in p)o(p,a)&&!o(u,a)&&(u[a]=p[a]);delete u[P];for(a in l)o(l,a)&&!o(u,a)&&(u[a]=l[a])}}else u={};return u&&"function"==typeof h&&(u=h(u),u&&"object"==typeof u||(u=null)),u},t.prototype.build=function(t){var e,r,n,i,a,p={},u={},f=this._options.preBuild;"function"==typeof f&&(t=f(t));for(r in t)if(o(t,r)&&null!==t[r]&&"undefined"!=typeof t[r]){for(n=!1,i=0,a=this._paramsMap.length;a>i;++i)if(this._paramsMap[i]===r){n=!0;break}n?p[r]=t[r]:u[r]=t[r]}return e=s.stringify(u),e&&(p[P]=e),this._buildFn(p)},t.prototype.getData=function(){return this._options.data},t.prototype.getName=function(){return this._options.data.name},r.exports=t,r.exports},"./router":function(){function t(){return this instanceof t?(this._routes=[],this._routesByName={},void 0):new t}var r={},n=e("./route");return t.prototype.addRoute=function(t){var e,r;return e=new n(t),this._routes.push(e),r=e.getName(),r&&(this._routesByName[r]=e),e},t.prototype.find=function(t){var e,r,n,o=[],i=this._routes;for(r=0,n=i.length;n>r;++r)e=i[r].match(t),null!==e&&o.push([i[r],e]);return o},t.prototype.findFirst=function(t){var e,r,n,o=this._routes;for(r=0,n=o.length;n>r;++r)if(e=o[r].match(t),null!==e)return[o[r],e];return null},t.prototype.getRouteByName=function(t){return this._routesByName[t]||null},t.Route=n,r.exports=t,r.exports}}[t]()}var r=e("./router"),n=!0;t.module&&"object"==typeof module.exports&&(module.exports=r,n=!1),t.modules&&modules.define&&modules.require&&(modules.define("susanin",function(t){t(r)}),n=!1),"function"==typeof t.define&&define.amd&&(define(function(){return r}),n=!1),n&&(t.Susanin=r)}(this); \ No newline at end of file +!function(t){function e(t){return{"./querystring":function(){var t={},e=Object.prototype.hasOwnProperty,r=Object.prototype.toString,n=function(t){return"[object Array]"===r.call(t)},o={decode:function(t){var e;try{e=decodeURIComponent(t.replace(/\+/g,"%20"))}catch(r){e=t}return e},parse:function(t,r,i){var a,s,p,u,f,l,h={};if("string"!=typeof t||""===t)return h;for(r||(r="&"),i||(i="="),a=t.split(r),f=0,l=a.length;l>f;++f)s=a[f].split(i),p="undefined"!=typeof s[1]?o.decode(s[1]):"",u=o.decode(s[0]),e.call(h,u)?n(h[u])?h[u].push(p):h[u]=[h[u],p]:h[u]=p;return h},stringify:function(t,r,n){var o,i,a,s,p,u,f="";if(!t)return f;r||(r="&"),n||(n="=");for(u in t)if(e.call(t,u))for(a=[].concat(t[u]),s=0,p=a.length;p>s;++s)i=typeof a[s],o="object"===i||"undefined"===i?"":encodeURIComponent(a[s]),f+=r+encodeURIComponent(u)+n+o;return f.substr(r.length)}};return t.exports=o,t.exports},"./route":function(){function t(e){if(!(this instanceof t))return new t(e);if("string"==typeof e&&(e={pattern:e}),!e||"object"!=typeof e)throw new Error("You must specify options");if("string"!=typeof e.pattern)throw new Error("You must specify the pattern of the route");this._options=e,e.conditions&&"object"==typeof e.conditions||(e.conditions={}),e.defaults&&"object"==typeof e.defaults||(e.defaults={}),e.isTrailingSlashOptional!==!1&&(e.pattern+=h+f+_+l+c,e.conditions[_]=b),e.pattern+=h+"?"+f+P+l+c,e.conditions[P]=".*",this._parts=this._parsePattern(e.pattern),this._buildParseRegExp(),this._buildBuildFn()}var r={},n=Object.prototype.hasOwnProperty,o=function(t,e){return n.call(t,e)},i=Object.prototype.toString,a=function(t){return"[object Array]"===i.call(t)},s=e("./querystring"),p=function(){var t=["/",".","*","+","?","|","(",")","[","]","{","}","\\"],e=new RegExp("(\\"+t.join("|\\")+")","g");return function(t){return t.replace(e,"\\$1")}}(),u=String(Math.random()).substr(2,5),f="<",l=">",h="(",c=")",d="[a-zA-Z_][\\w\\-]*",m="[\\w\\-\\.~]+",y=new RegExp("("+p(f)+d+p(l)+"|"+"[^"+p(f)+p(l)+"]+"+"|"+p(f)+"|"+p(l)+")","g"),_="ts_"+u,g="/",b=p("/"),P="qs_"+u;return t.prototype._parsePattern=function(t){for(var e,r,n,o=[],i="",a=0,s=0,p=!1,u=t.length;u>a;)if(e=t.charAt(a++),e===h)p?(++s,i+=e):(this._parseParams(i,o),i="",s=0,p=!0);else if(e===c)if(p)if(0===s){for(i={what:"optional",dependOnParams:[],parts:this._parsePattern(i)},o.push(i),r=0,n=i.parts.length;n>r;++r)i.parts[r]&&"param"===i.parts[r].what&&i.dependOnParams.push(i.parts[r].name);i="",p=!1}else--s,i+=e;else i+=e;else i+=e;return this._parseParams(i,o),o},t.prototype._parseParams=function(t,e){var r,n,o,i=t.match(y);if(i)for(r=0,n=i.length;n>r;++r)o=i[r],o.charAt(0)===f&&o.charAt(o.length-1)===l?e.push({what:"param",name:o.substr(1,o.length-2)}):e.push(o)},t.prototype._buildParseRegExp=function(){this._reqExpParamsMap=[],this._parseRegExpSource="^"+this._buildParseRegExpParts(this._parts)+"$",this._parseRegExp=new RegExp(this._parseRegExpSource)},t.prototype._buildParseRegExpParts=function(t){var e,r,n,o="";for(e=0,r=t.length;r>e;++e)n=t[e],"string"==typeof n?o+=p(n):"param"===n.what?(this._reqExpParamsMap.push(n.name),o+="("+this._buildParamValueRegExpSource(n.name)+")"):o+="(?:"+this._buildParseRegExpParts(n.parts)+")?";return o},t.prototype._buildParamValueRegExpSource=function(t){var e,r=this._options.conditions[t];return e=r?a(r)?"(?:"+r.join("|")+")":r+"":m},t.prototype._buildBuildFn=function(){this._mainParamsMap={},this._buildFnSource="var h=({}).hasOwnProperty;return "+this._buildBuildFnParts(this._parts)+";",this._buildFn=new Function("p",this._buildFnSource)},t.prototype._buildBuildFnParts=function(t){var e,r,n,i,a,s,u='""',f=this._options.defaults;for(e=0,r=t.length;r>e;++e)if(a=t[e],"string"==typeof a)u+='+"'+p(a)+'"';else if("param"===a.what)this._mainParamsMap[a.name]=!0,u+='+(h.call(p,"'+p(a.name)+'")?'+'p["'+p(a.name)+'"]:'+(o(f,a.name)?'"'+p(f[a.name])+'"':'""')+")";else{for(u+="+((false",n=0,i=a.dependOnParams.length;i>n;++n)s=a.dependOnParams[n],u+='||(h.call(p,"'+p(s)+'")'+(o(f,s)?'&&p["'+p(s)+'"]!=="'+p(f[s])+'"':"")+")";u+=")?("+this._buildBuildFnParts(a.parts)+'):"")'}return u},t.prototype._isDataMatched=function(t){var e,r=this._options.data;if("function"==typeof t)return Boolean(t(r));if(t&&"object"==typeof t)for(e in t)if(o(t,e)&&(!r||"object"!=typeof r||r[e]!==t[e]))return!1;return!0},t.prototype.match=function(t,e){var r,n,i,a,p,u,f=null,l=this._options,h=l.postMatch,c=l.defaults;if("string"!=typeof t||e&&!this._isDataMatched(e))return f;if(n=t.match(this._parseRegExp)){for(f={},i=1,a=n.length;a>i;++i)if("undefined"!=typeof n[i]&&""!==n[i])if(r=this._reqExpParamsMap[i-1],r!==_)f[r]=n[i];else if(t.charAt(t.length-2)===g)return null;u=s.parse(f[P]);for(p in u)o(u,p)&&!o(f,p)&&(f[p]=u[p]);delete f[P];for(p in c)o(c,p)&&!o(f,p)&&(f[p]=c[p])}return f&&"function"==typeof h&&(f=h(f),f&&"object"==typeof f||(f=null)),f},t.prototype.build=function(t){var e,r,n={},i={},a=this._options.preBuild;"function"==typeof a&&(t=a(t));for(r in t)o(t,r)&&null!==t[r]&&"undefined"!=typeof t[r]&&(this._mainParamsMap[r]?n[r]=t[r]:i[r]=t[r]);return e=s.stringify(i),e&&(n[P]=e),this._buildFn(n)},t.prototype.getData=function(){return this._options.data},t.prototype.getName=function(){return this._options.name},r.exports=t,r.exports},"./router":function(){function t(){return this instanceof t?(this._routes=[],this._routesByName={},void 0):new t}var r={},n=e("./route");return t.prototype.addRoute=function(t){var e,r;return e=new n(t),this._routes.push(e),r=e.getName(),r&&(this._routesByName[r]=e),e},t.prototype.find=function(){var t,e,r,n=[],o=this._routes;for(e=0,r=o.length;r>e;++e)t=o[e].match.apply(o[e],arguments),null!==t&&n.push([o[e],t]);return n},t.prototype.findFirst=function(){var t,e,r,n=this._routes;for(e=0,r=n.length;r>e;++e)if(t=n[e].match.apply(n[e],arguments),null!==t)return[n[e],t];return null},t.prototype.getRouteByName=function(t){return this._routesByName[t]||null},t.Route=n,r.exports=t,r.exports}}[t]()}var r=e("./router"),n=!0;t.module&&"object"==typeof module.exports&&(module.exports=r,n=!1),t.modules&&modules.define&&modules.require&&(modules.define("susanin",function(t){t(r)}),n=!1),"function"==typeof t.define&&define.amd&&(define(function(){return r}),n=!1),n&&(t.Susanin=r)}(this); \ No newline at end of file diff --git a/karma.conf.js b/karma.conf.js index 8450427..9d9b102 100644 --- a/karma.conf.js +++ b/karma.conf.js @@ -15,7 +15,7 @@ module.exports = function(config) { files : [ 'dist/susanin.min.js', 'node_modules/chai/chai.js', - 'test/lib/browser.js', + 'test/browser/*.js', 'lib/querystring.js', 'test/*.js' ], diff --git a/lib/route.js b/lib/route.js index aeffb8c..0e9c25d 100644 --- a/lib/route.js +++ b/lib/route.js @@ -85,8 +85,6 @@ function Route(options) { options.conditions && typeof options.conditions === 'object' || (options.conditions = {}); options.defaults && typeof options.defaults === 'object' || (options.defaults = {}); - options.data && typeof options.data === 'object' || (options.data = {}); - typeof options.name === 'string' && (options.data.name = options.name); if (options.isTrailingSlashOptional !== false) { options.pattern += GROUP_OPENED_CHAR + PARAM_OPENED_CHAR + @@ -206,7 +204,7 @@ Route.prototype._parseParams = function(pattern, parts) { * @private */ Route.prototype._buildParseRegExp = function() { - this._paramsMap = []; + this._reqExpParamsMap = []; this._parseRegExpSource = '^' + this._buildParseRegExpParts(this._parts) + '$'; this._parseRegExp = new RegExp(this._parseRegExpSource); }; @@ -226,10 +224,10 @@ Route.prototype._buildParseRegExpParts = function(parts) { if (typeof part === 'string') { ret += escape(part); - } else if (part && part.what === 'param') { - this._paramsMap.push(part.name); + } else if (part.what === 'param') { + this._reqExpParamsMap.push(part.name); ret += '(' + this._buildParamValueRegExpSource(part.name) + ')'; - } else if (part && part.what === 'optional') { + } else { ret += '(?:' + this._buildParseRegExpParts(part.parts) + ')?'; } } @@ -263,6 +261,7 @@ Route.prototype._buildParamValueRegExpSource = function(paramName) { * @private */ Route.prototype._buildBuildFn = function() { + this._mainParamsMap = {}; this._buildFnSource = 'var h=({}).hasOwnProperty;return ' + this._buildBuildFnParts(this._parts) + ';'; /*jshint evil:true */ this._buildFn = new Function('p', this._buildFnSource); @@ -284,14 +283,15 @@ Route.prototype._buildBuildFnParts = function(parts) { if (typeof part === 'string') { ret += '+"' + escape(part) + '"' ; - } else if (part && part.what === 'param') { + } else if (part.what === 'param') { + this._mainParamsMap[part.name] = true; ret += '+(h.call(p,"' + escape(part.name) + '")?' + 'p["' + escape(part.name) + '"]:' + (has(defaults, part.name) ? '"' + escape(defaults[part.name]) + '"' : '""') + ')'; - } else if (part && part.what === 'optional') { + } else { ret += '+((false'; for (j = 0, sizeJ = part.dependOnParams.length; j < sizeJ; ++j) { @@ -313,11 +313,36 @@ Route.prototype._buildBuildFnParts = function(parts) { }; /** - * Matches object with route - * @param {Object|String} matchObject + * @param {Object|Function} data + * @returns {Boolean} + * @private + */ +Route.prototype._isDataMatched = function(data) { + var routeData = this._options.data, + key; + + if (typeof data === 'function') { + return Boolean(data(routeData)); + } else if (data && typeof data === 'object') { + for (key in data) { + if (has(data, key)) { + if ( ! routeData || typeof routeData !== 'object' || routeData[key] !== data[key]) { + return false; + } + } + } + } + + return true; +}; + +/** + * Matches path with route + * @param {String} path + * @param {Function|Object} [data] * @returns {Object|null} */ -Route.prototype.match = function(matchObject) { +Route.prototype.match = function(path, data) { var ret = null, paramName, matches, @@ -328,55 +353,39 @@ Route.prototype.match = function(matchObject) { filter = options.postMatch, defaults = options.defaults; - if (typeof matchObject === 'string') { - matchObject = { path : matchObject }; - } else if ( ! matchObject) { + if (typeof path !== 'string' || (data && ! this._isDataMatched(data))) { return ret; } - for (key in matchObject) { - if (has(matchObject, key) && key !== 'path') { - if (options.data[key] !== matchObject[key]) { - return ret; - } - } - } + matches = path.match(this._parseRegExp); - if (typeof matchObject.path === 'string') { - matches = matchObject.path.match(this._parseRegExp); - - if (matches) { - ret = {}; - - for (i = 1, size = matches.length; i < size; ++i) { - if (typeof matches[i] !== 'undefined' && /* for IE lt 9*/ matches[i] !== '') { - paramName = this._paramsMap[i - 1]; - if (paramName !== TRAILING_SLASH_PARAM_NAME) { - ret[paramName] = matches[i]; - } else if ( - matchObject.path.charAt(matchObject.path.length - 2) === TRAILING_SLASH_PARAM_VALUE - ) { - return null; - } + if (matches) { + ret = {}; + + for (i = 1, size = matches.length; i < size; ++i) { + if (typeof matches[i] !== 'undefined' && /* for IE lt 9*/ matches[i] !== '') { + paramName = this._reqExpParamsMap[i - 1]; + if (paramName !== TRAILING_SLASH_PARAM_NAME) { + ret[paramName] = matches[i]; + } else if (path.charAt(path.length - 2) === TRAILING_SLASH_PARAM_VALUE) { + return null; } } + } - queryParams = querystring.parse(ret[QUERY_STRING_PARAM_NAME]); - for (key in queryParams) { - if (has(queryParams, key) && ! has(ret, key)) { - ret[key] = queryParams[key]; - } + queryParams = querystring.parse(ret[QUERY_STRING_PARAM_NAME]); + for (key in queryParams) { + if (has(queryParams, key) && ! has(ret, key)) { + ret[key] = queryParams[key]; } - delete ret[QUERY_STRING_PARAM_NAME]; + } + delete ret[QUERY_STRING_PARAM_NAME]; - for (key in defaults) { - if (has(defaults, key) && ! has(ret, key)) { - ret[key] = defaults[key]; - } + for (key in defaults) { + if (has(defaults, key) && ! has(ret, key)) { + ret[key] = defaults[key]; } } - } else { - ret = {}; } if (ret && typeof filter === 'function') { @@ -399,8 +408,6 @@ Route.prototype.build = function(params) { queryParams = {}, queryString, key, - isMainParam, - i, size, filter = this._options.preBuild; if (typeof filter === 'function') { @@ -413,15 +420,7 @@ Route.prototype.build = function(params) { params[key] !== null && typeof params[key] !== 'undefined' ) { - isMainParam = false; - for (i = 0, size = this._paramsMap.length; i < size; ++i) { - if (this._paramsMap[i] === key) { - isMainParam = true; - break; - } - } - - if (isMainParam) { + if (this._mainParamsMap[key]) { newParams[key] = params[key]; } else { queryParams[key] = params[key]; @@ -445,10 +444,10 @@ Route.prototype.getData = function() { /** * Returns name of the route - * @returns {?String} + * @returns {*} */ Route.prototype.getName = function() { - return this._options.data.name; + return this._options.name; }; module.exports = Route; diff --git a/lib/router.js b/lib/router.js index 29b1e64..b1fe8a0 100644 --- a/lib/router.js +++ b/lib/router.js @@ -15,7 +15,7 @@ function Router() { /** * Add route - * @param {Object|String} options + * @param {RouteOptions} options * @returns {Route} */ Router.prototype.addRoute = function(options) { @@ -33,17 +33,16 @@ Router.prototype.addRoute = function(options) { /** * Returns all successfully matched routes - * @param {Object|String} matchObject - * @returns {Array|null} + * @returns {[ Route, Object ][]} */ -Router.prototype.find = function(matchObject) { +Router.prototype.find = function() { var ret = [], parsed, i, size, routes = this._routes; for (i = 0, size = routes.length; i < size; ++i) { - parsed = routes[i].match(matchObject); + parsed = routes[i].match.apply(routes[i], arguments); if (parsed !== null) { ret.push([ routes[i], parsed ]); } @@ -54,16 +53,15 @@ Router.prototype.find = function(matchObject) { /** * Returns first successfully matched route - * @param {Object|String} matchObject - * @returns {Array|null} + * @returns {[ Route, Object ]|null} */ -Router.prototype.findFirst = function(matchObject) { +Router.prototype.findFirst = function() { var parsed, i, size, routes = this._routes; for (i = 0, size = routes.length; i < size; ++i) { - parsed = routes[i].match(matchObject); + parsed = routes[i].match.apply(routes[i], arguments); if (parsed !== null) { return [ routes[i], parsed ]; } diff --git a/test/lib/browser.js b/test/browser/setup.js similarity index 100% rename from test/lib/browser.js rename to test/browser/setup.js diff --git a/test/data_matching.js b/test/data_matching.js new file mode 100644 index 0000000..bf7cfed --- /dev/null +++ b/test/data_matching.js @@ -0,0 +1,73 @@ +/* global describe, it, Router, assert */ + +describe('route.match() with data', function() { + var Route = Router.Route; + + it('routeData is absent', function(done) { + var route = Route('/opa'); + + assert.deepEqual(route.match('/opapa'), null); + assert.deepEqual(route.match('/opa'), {}); + assert.deepEqual(route.match('/opa', {}), {}); + assert.deepEqual(route.match('/opa', { method : 'get' }), null); + + done(); + }); + + it('data is an object', function(done) { + function Data() {} + Data.prototype = { method : 'post' }; + + var routeData = { + method : 'get', + foo : [ 'bar1', 'bar2' ] + }, + route = Route({ + pattern : '/opa', + data : routeData + }); + + assert.deepEqual(route.match('/opapa'), null); + assert.deepEqual(route.match('/opa'), {}); + assert.deepEqual(route.match('/opa', null), {}); + assert.deepEqual(route.match('/opa', {}), {}); + assert.deepEqual(route.match('/opa', 1), {}); + assert.deepEqual(route.match('/opa', 'a'), {}); + assert.deepEqual(route.match('/opa', { method : 'post' }), null); + assert.deepEqual(route.match('/opa', { method : 'get' }), {}); + assert.deepEqual(route.match('/opa', { foo : 'bar1' }), null); + assert.deepEqual(route.match('/opa', { foo : 'bar2' }), null); + assert.deepEqual(route.match('/opa', { foo : [ 'bar1', 'bar2' ] }), null); + assert.deepEqual(route.match('/opa', { foo : routeData.foo }), {}); + assert.deepEqual(route.match('/opa', { method : 'get', foo : routeData.foo }), {}); + assert.deepEqual(route.match('/opa', { method : 'post', foo : routeData.foo }), null); + assert.deepEqual(route.match('/opa', new Data()), {}); + + done(); + }); + + it('data is a function', function(done) { + var routeData = { + method : 'get', + foo : [ 'bar1', 'bar2' ] + }, + route = Route({ + pattern : '/opa', + data : routeData + }), + undef; + + [ true, false, 0, 1, '', 'a', undef, {} ].forEach(function(value) { + assert.deepEqual(route.match('/opa', function() { + return value; + }), Boolean(value) ? {} : null); + }); + + route.match('/opa', function(data) { + assert.deepEqual(data, routeData); + }); + + done(); + }); + +}); diff --git a/test/lib/nodejs.js b/test/nodejs/setup.js similarity index 66% rename from test/lib/nodejs.js rename to test/nodejs/setup.js index 8d83173..04b96f7 100644 --- a/test/lib/nodejs.js +++ b/test/nodejs/setup.js @@ -1,3 +1,3 @@ -global.Router = require('../..'); +global.Router = require('../../lib/router.js'); global.assert = require('chai').assert; global.querystring = require('../../lib/querystring'); diff --git a/test/querystring.js b/test/querystring.js index 3d8cd27..6f0b545 100644 --- a/test/querystring.js +++ b/test/querystring.js @@ -30,6 +30,11 @@ describe('querystring module', function() { }); it('route.stringify()', function(done) { + function Params() { + this.bla = 'foo'; + } + Params.prototype = { bla1 : 'foo1' }; + assert.strictEqual(qs.stringify(), ''); assert.strictEqual(qs.stringify(null), ''); assert.strictEqual(qs.stringify(undef), ''); @@ -48,6 +53,7 @@ describe('querystring module', function() { assert.strictEqual(qs.stringify({ bla : [ 'foo1', 'foo2' ] }), 'bla=foo1&bla=foo2'); assert.strictEqual(qs.stringify({ bla : 'foo', bla1 : 'foo1' }), 'bla=foo&bla1=foo1'); assert.strictEqual(qs.stringify({ bla : 'foo', bla1 : 'foo1' }), 'bla=foo&bla1=foo1'); + assert.strictEqual(qs.stringify(new Params()), 'bla=foo'); assert.strictEqual(qs.stringify([ 1, 2, 3 ]), '0=1&1=2&2=3'); done(); diff --git a/test/route.getData.js b/test/route.getData.js index 46178ac..db8f9a7 100644 --- a/test/route.getData.js +++ b/test/route.getData.js @@ -4,8 +4,10 @@ describe('route.getData()', function() { var Route = Router.Route; it('route.getData() must return right data', function(done) { - assert.deepEqual(Route('/opa').getData(), {}); - assert.deepEqual(Route({ pattern : '/opa' }).getData(), {}); + var undef; + + assert.deepEqual(Route('/opa').getData(), undef); + assert.deepEqual(Route({ pattern : '/opa' }).getData(), undef); assert.deepEqual(Route({ pattern : '/opa', data : { foo : 'bar' } }).getData(), { foo : 'bar' }); done(); diff --git a/test/route.getName.js b/test/route.getName.js index e586b8c..8cd0746 100644 --- a/test/route.getName.js +++ b/test/route.getName.js @@ -4,9 +4,10 @@ describe('route.getName()', function() { var Route = Router.Route; it('route.getName() must return right data', function(done) { - assert.strictEqual(Route('/opa').getName(), undefined); + var undef; + + assert.strictEqual(Route('/opa').getName(), undef); assert.strictEqual(Route({ pattern : '/opa', name : 'opa' }).getName(), 'opa'); - assert.strictEqual(Route({ pattern : '/opa', data : { name : 'opa' } }).getName(), 'opa'); done(); }); diff --git a/test/route.match.js b/test/route.match.js index 01fa126..8c2ec84 100644 --- a/test/route.match.js +++ b/test/route.match.js @@ -133,6 +133,23 @@ describe('route.match()', function() { done(); }); + it('/opa)(/opapa/)', function(done) { + var route = Route({ + pattern : '/opa)(/opapa/)' + }); + + assert.deepEqual(route.match('/opa'), null); + assert.deepEqual(route.match('/opa)/'), {}); + assert.deepEqual(route.match('/opa)'), {}); + assert.deepEqual(route.match('/opa)/value'), null); + assert.deepEqual(route.match('/opa)/opapa/'), null); + assert.deepEqual(route.match('/opa)/opapa/value'), { param : 'value' }); + assert.deepEqual(route.match('/opa)?foo1=bar1&foo1=bar2&foo2=&=bar3'), + { foo1 : [ 'bar1', 'bar2' ], foo2 : '', '' : 'bar3' }); + + done(); + }); + it('/opa(/opapa/) and defaults', function(done) { var route = Route({ pattern : '/opa(/opapa/)', diff --git a/test/router.addRoute.js b/test/router.addRoute.js index bf36495..86bb36b 100644 --- a/test/router.addRoute.js +++ b/test/router.addRoute.js @@ -1,6 +1,6 @@ /* global describe, it, Router, assert */ -describe('route.match()', function() { +describe('route.addRoute()', function() { var Route = Router.Route; it('Instance of `Router` must have function `addRoute`', function(done) { diff --git a/test/router.find.js b/test/router.find.js index 53aa38e..0facf46 100644 --- a/test/router.find.js +++ b/test/router.find.js @@ -41,7 +41,7 @@ describe('router.find*', function() { assert.strictEqual(finded[0], this.router.getRouteByName('first')); assert.deepEqual(finded[1], {}); - assert.strictEqual(this.router.findFirst({ path : '/first', method : 'post' })[0], this.router.getRouteByName('third')); + assert.strictEqual(this.router.findFirst('/first', { method : 'post' })[0], this.router.getRouteByName('third')); assert.strictEqual(this.router.findFirst('/f'), null); done(); @@ -57,7 +57,7 @@ describe('router.find*', function() { assert.deepEqual(finded[0][1], {}); assert.deepEqual(finded[1][1], {}); assert.deepEqual(finded[2][1], {}); - assert.strictEqual(this.router.find({ path : '/first', method : 'post' })[0][0], this.router.getRouteByName('third')); + assert.strictEqual(this.router.find('/first', { method : 'post' })[0][0], this.router.getRouteByName('third')); assert.deepEqual(this.router.find('/f'), []); done(); diff --git a/test/router.getRouteByName.js b/test/router.getRouteByName.js index 0bbfb53..22fcf51 100644 --- a/test/router.getRouteByName.js +++ b/test/router.getRouteByName.js @@ -4,8 +4,8 @@ describe('router.getRouteByName()', function() { it('`getRouteByName` must return right instance of `Route`', function(done) { var router = Router(), - routeFoo = router.addRoute({ pattern : '/foo', data : { name : 'foo' } }), - routeBar = router.addRoute({ pattern : '/bar', data : { name : 'bar' } }); + routeFoo = router.addRoute({ pattern : '/foo', name : 'foo' }), + routeBar = router.addRoute({ pattern : '/bar', name : 'bar' }); assert.strictEqual(router.getRouteByName('bar'), routeBar); assert.strictEqual(router.getRouteByName('foo'), routeFoo);