Skip to content

Commit 450b1f0

Browse files
committed
feat($urlMatcherFactory): implement type support
1 parent 730b76f commit 450b1f0

File tree

2 files changed

+249
-128
lines changed

2 files changed

+249
-128
lines changed

src/urlMatcherFactory.js

+128-43
Original file line numberDiff line numberDiff line change
@@ -72,15 +72,14 @@ function UrlMatcher(pattern, caseInsensitiveMatch) {
7272
// \\. - a backslash escape
7373
// \{(?:[^{}\\]+|\\.)*\} - a matched set of curly braces containing other atoms
7474
var placeholder = /([:*])(\w+)|\{(\w+)(?:\:((?:[^{}\\]+|\\.|\{(?:[^{}\\]+|\\.)*\})+))?\}/g,
75-
names = {}, compiled = '^', last = 0, m,
75+
compiled = '^', last = 0, m,
7676
segments = this.segments = [],
77-
params = this.params = [];
77+
params = this.params = {};
7878

79-
function addParameter(id) {
79+
function addParameter(id, type) {
8080
if (!/^\w+(-+\w+)*$/.test(id)) throw new Error("Invalid parameter name '" + id + "' in pattern '" + pattern + "'");
81-
if (names[id]) throw new Error("Duplicate parameter name '" + id + "' in pattern '" + pattern + "'");
82-
names[id] = true;
83-
params.push(id);
81+
if (params[id]) throw new Error("Duplicate parameter name '" + id + "' in pattern '" + pattern + "'");
82+
params[id] = type;
8483
}
8584

8685
function quoteRegExp(string) {
@@ -91,25 +90,30 @@ function UrlMatcher(pattern, caseInsensitiveMatch) {
9190

9291
// Split into static segments separated by path parameter placeholders.
9392
// The number of segments is always 1 more than the number of parameters.
94-
var id, regexp, segment;
93+
var id, regexp, segment, type;
94+
9595
while ((m = placeholder.exec(pattern))) {
96-
id = m[2] || m[3]; // IE[78] returns '' for unmatched groups instead of null
97-
regexp = m[4] || (m[1] == '*' ? '.*' : '[^/]*');
96+
id = m[2] || m[3]; // IE[78] returns '' for unmatched groups instead of null
97+
regexp = m[4] || (m[1] == '*' ? '.*' : '[^/]*');
9898
segment = pattern.substring(last, m.index);
99+
type = this.$types[regexp] || new Type({ pattern: new RegExp(regexp) });
100+
99101
if (segment.indexOf('?') >= 0) break; // we're into the search part
100-
compiled += quoteRegExp(segment) + '(' + regexp + ')';
101-
addParameter(id);
102+
103+
compiled += quoteRegExp(segment) + '(' + type.$subPattern() + ')';
104+
addParameter(id, type);
102105
segments.push(segment);
103106
last = placeholder.lastIndex;
104107
}
105108
segment = pattern.substring(last);
106109

107110
// Find any search parameter names and remove them from the last segment
108111
var i = segment.indexOf('?');
112+
109113
if (i >= 0) {
110114
var search = this.sourceSearch = segment.substring(i);
111115
segment = segment.substring(0, i);
112-
this.sourcePath = pattern.substring(0, last+i);
116+
this.sourcePath = pattern.substring(0, last + i);
113117

114118
// Allow parameters to be separated by '?' as well as '&' to make concat() easier
115119
forEach(search.substring(1).split(/[&?]/), addParameter);
@@ -120,12 +124,8 @@ function UrlMatcher(pattern, caseInsensitiveMatch) {
120124

121125
compiled += quoteRegExp(segment) + '$';
122126
segments.push(segment);
123-
if(caseInsensitiveMatch){
124-
this.regexp = new RegExp(compiled, 'i');
125-
}else{
126-
this.regexp = new RegExp(compiled);
127-
}
128-
127+
128+
this.regexp = (caseInsensitiveMatch) ? new RegExp(compiled, 'i') : new RegExp(compiled);
129129
this.prefix = segments[0];
130130
}
131131

@@ -187,14 +187,18 @@ UrlMatcher.prototype.exec = function (path, searchParams) {
187187
var m = this.regexp.exec(path);
188188
if (!m) return null;
189189

190-
var params = this.params, nTotal = params.length,
191-
nPath = this.segments.length-1,
192-
values = {}, i;
190+
var params = this.parameters(), nTotal = params.length,
191+
nPath = this.segments.length - 1,
192+
values = {}, i, type, param;
193193

194194
if (nPath !== m.length - 1) throw new Error("Unbalanced capture group in route '" + this.source + "'");
195195

196-
for (i=0; i<nPath; i++) values[params[i]] = m[i+1];
197-
for (/**/; i<nTotal; i++) values[params[i]] = searchParams[params[i]];
196+
for (i = 0; i < nPath; i++) {
197+
param = params[i];
198+
type = this.params[param];
199+
values[param] = type.decode(m[i + 1]);
200+
}
201+
for (/**/; i < nTotal; i++) values[params[i]] = searchParams[params[i]];
198202

199203
return values;
200204
};
@@ -211,7 +215,7 @@ UrlMatcher.prototype.exec = function (path, searchParams) {
211215
* pattern has no parameters, an empty array is returned.
212216
*/
213217
UrlMatcher.prototype.parameters = function () {
214-
return this.params;
218+
return keys(this.params);
215219
};
216220

217221
/**
@@ -234,30 +238,59 @@ UrlMatcher.prototype.parameters = function () {
234238
* @returns {string} the formatted URL (path and optionally search part).
235239
*/
236240
UrlMatcher.prototype.format = function (values) {
237-
var segments = this.segments, params = this.params;
241+
var segments = this.segments, params = this.parameters();
238242
if (!values) return segments.join('');
239243

240-
var nPath = segments.length-1, nTotal = params.length,
241-
result = segments[0], i, search, value;
244+
var nPath = segments.length - 1, nTotal = params.length,
245+
result = segments[0], i, search, value, param, type;
242246

243-
for (i=0; i<nPath; i++) {
244-
value = values[params[i]];
245-
// TODO: Maybe we should throw on null here? It's not really good style to use '' and null interchangeabley
246-
if (value != null) result += encodeURIComponent(value);
247-
result += segments[i+1];
248-
}
249-
for (/**/; i<nTotal; i++) {
250-
value = values[params[i]];
251-
if (value != null) {
252-
result += (search ? '&' : '?') + params[i] + '=' + encodeURIComponent(value);
253-
search = true;
254-
}
247+
for (i = 0; i < nPath; i++) {
248+
param = params[i];
249+
value = values[param];
250+
type = this.params[param];
251+
// TODO: Maybe we should throw on null here? It's not really good style
252+
// to use '' and null interchangeabley
253+
if (value != null) result += encodeURIComponent(type.encode(value));
254+
result += segments[i + 1];
255255
}
256256

257+
for (/**/; i < nTotal; i++) {
258+
param = params[i]
259+
if (values[param] == null) continue;
260+
result += (search ? '&' : '?') + param + '=' + encodeURIComponent(values[param]);
261+
search = true;
262+
}
257263
return result;
258264
};
259265

266+
UrlMatcher.prototype.$types = {};
267+
268+
function Type(options) {
269+
extend(this, options);
270+
}
271+
272+
Type.prototype.is = function(val, key) {
273+
return angular.toJson(this.decode(this.encode(val))) === angular.toJson(val);
274+
};
275+
276+
Type.prototype.encode = function(val, key) {
277+
return String(val);
278+
};
260279

280+
Type.prototype.decode = function(val, key) {
281+
return val;
282+
}
283+
284+
Type.prototype.equals = function(a, b) {
285+
return a == b;
286+
};
287+
288+
Type.prototype.$subPattern = function() {
289+
var sub = this.pattern.toString();
290+
return sub.substr(1, sub.length - 2);
291+
};
292+
293+
Type.prototype.pattern = /.*/;
261294

262295
/**
263296
* @ngdoc object
@@ -271,6 +304,33 @@ function $UrlMatcherFactory() {
271304

272305
var useCaseInsensitiveMatch = false;
273306

307+
var enqueue = true, typeQueue = [], injector, defaultTypes = {
308+
int: {
309+
decode: function(val) {
310+
return parseInt(val, 10);
311+
},
312+
is: function(val) {
313+
return this.decode(val.toString()) === val;
314+
},
315+
pattern: /\d+/
316+
},
317+
bool: {
318+
encode: function(val) {
319+
return val ? 1 : 0;
320+
},
321+
decode: function(val) {
322+
return parseInt(val, 10) === 0 ? false : true;
323+
},
324+
is: function(val) {
325+
return val === true || val === false;
326+
},
327+
pattern: /0|1/
328+
},
329+
string: {
330+
pattern: /.*/
331+
}
332+
};
333+
274334
/**
275335
* @ngdoc function
276336
* @name ui.router.util.$urlMatcherFactory#caseInsensitiveMatch
@@ -281,7 +341,7 @@ function $UrlMatcherFactory() {
281341
*
282342
* @param {bool} value false to match URL in a case sensitive manner; otherwise true;
283343
*/
284-
this.caseInsensitiveMatch = function(value){
344+
this.caseInsensitiveMatch = function(value) {
285345
useCaseInsensitiveMatch = value;
286346
};
287347

@@ -314,11 +374,36 @@ function $UrlMatcherFactory() {
314374
this.isMatcher = function (o) {
315375
return isObject(o) && isFunction(o.exec) && isFunction(o.format) && isFunction(o.concat);
316376
};
317-
318-
/* No need to document $get, since it returns this */
319-
this.$get = function () {
377+
378+
this.type = function (name, def) {
379+
if (!isDefined(def)) return UrlMatcher.prototype.$types[name];
380+
typeQueue.push({ name: name, def: def });
381+
if (!enqueue) flushTypeQueue();
320382
return this;
321383
};
384+
385+
/* No need to document $get, since it returns this */
386+
this.$get = ['$injector', function ($injector) {
387+
injector = $injector;
388+
enqueue = false;
389+
UrlMatcher.prototype.$types = {};
390+
flushTypeQueue();
391+
392+
forEach(defaultTypes, function(type, name) {
393+
if (!UrlMatcher.prototype.$types[name]) UrlMatcher.prototype.$types[name] = new Type(type);
394+
});
395+
return this;
396+
}];
397+
398+
function flushTypeQueue() {
399+
forEach(typeQueue, function(type) {
400+
if (UrlMatcher.prototype.$types[type.name]) {
401+
throw new Error("A type named '" + type.name + "' has already been defined.");
402+
}
403+
var def = new Type(isFunction(type.def) ? injector.invoke(type.def) : type.def);
404+
UrlMatcher.prototype.$types[type.name] = def;
405+
});
406+
}
322407
}
323408

324409
// Register as a provider so it's available to other providers

0 commit comments

Comments
 (0)