Skip to content

Commit

Permalink
fix($urlMatcherFactory): add Type.$normalize function
Browse files Browse the repository at this point in the history
- Type.$normalize can be passed either a decoded object, or an encoded object (string).  If the parameter is already decoded (checked via .is()), then it is returned. Else the parameter is decoded and returned.
  • Loading branch information
christopherthielen committed Nov 24, 2014
1 parent 5797c2e commit b0c6aa2
Show file tree
Hide file tree
Showing 2 changed files with 37 additions and 19 deletions.
42 changes: 29 additions & 13 deletions src/urlMatcherFactory.js
Original file line number Diff line number Diff line change
Expand Up @@ -480,6 +480,11 @@ Type.prototype.pattern = /.*/;

Type.prototype.toString = function() { return "{Type:" + this.name + "}"; };

/** Given an encoded string, or a decoded object, returns a decoded object */
Type.prototype.$normalize = function(val) {
return this.is(val) ? val : this.decode(val);
};

/*
* Wraps an existing custom Type as an array of Type, depending on 'mode'.
* e.g.:
Expand All @@ -493,7 +498,6 @@ Type.prototype.toString = function() { return "{Type:" + this.name + "}"; };
Type.prototype.$asArray = function(mode, isSearch) {
if (!mode) return this;
if (mode === "auto" && !isSearch) throw new Error("'auto' array mode is for query parameters only");
return new ArrayType(this, mode);

function ArrayType(type, mode) {
function bindTo(type, callbackName) {
Expand Down Expand Up @@ -542,8 +546,12 @@ Type.prototype.$asArray = function(mode, isSearch) {
this.is = arrayHandler(bindTo(type, 'is'), true);
this.equals = arrayEqualsHandler(bindTo(type, 'equals'));
this.pattern = type.pattern;
this.$normalize = arrayHandler(bindTo(type, '$normalize'));
this.name = type.name;
this.$arrayMode = mode;
}

return new ArrayType(this, mode);
};


Expand Down Expand Up @@ -571,7 +579,7 @@ function $UrlMatcherFactory() {
string: {
encode: valToString,
decode: valFromString,
is: regexpMatches,
is: function(val) { return typeof val === "string"},
pattern: /[^/]*/
},
int: {
Expand Down Expand Up @@ -945,7 +953,10 @@ function $UrlMatcherFactory() {
*/
function $$getDefaultValue() {
if (!injector) throw new Error("Injectable functions cannot be called at configuration time");
return injector.invoke(config.$$fn);
var defaultValue = injector.invoke(config.$$fn);
if (defaultValue !== null && defaultValue !== undefined && !self.type.is(defaultValue))
throw new Error("Default value (" + defaultValue + ") for parameter '" + self.id + "' is not an instance of Type (" + self.type.name + ")");
return defaultValue;
}

/**
Expand All @@ -959,7 +970,7 @@ function $UrlMatcherFactory() {
return replacement.length ? replacement[0] : value;
}
value = $replace(value);
return isDefined(value) ? self.type.decode(value) : $$getDefaultValue();
return !isDefined(value) ? $$getDefaultValue() : self.type.$normalize(value);
}

function toString() { return "{Param:" + id + " " + type + " squash: '" + squash + "' optional: " + isOptional + "}"; }
Expand Down Expand Up @@ -1015,15 +1026,20 @@ function $UrlMatcherFactory() {
return equal;
},
$$validates: function $$validate(paramValues) {
var result = true, isOptional, val, param, self = this;

forEach(this.$$keys(), function(key) {
param = self[key];
val = paramValues[key];
isOptional = !val && param.isOptional;
result = result && (isOptional || !!param.type.is(val));
});
return result;
var keys = this.$$keys(), i, param, rawVal, normalized, encoded;
for (i = 0; i < keys.length; i++) {
param = this[keys[i]];
rawVal = paramValues[keys[i]];
if ((rawVal === undefined || rawVal === null) && param.isOptional)
break; // There was no parameter value, but the param is optional
normalized = param.type.$normalize(rawVal);
if (!param.type.is(normalized))
return false; // The value was not of the correct Type, and could not be decoded to the correct Type
encoded = param.type.encode(normalized);
if (angular.isString(encoded) && !param.type.pattern.exec(encoded))
return false; // The value was of the correct type, but when encoded, did not match the Type's regexp
}
return true;
},
$$parent: undefined
};
Expand Down
14 changes: 8 additions & 6 deletions test/urlMatcherFactorySpec.js
Original file line number Diff line number Diff line change
Expand Up @@ -407,15 +407,17 @@ describe("urlMatcherFactoryProvider", function () {
var m;
beforeEach(module('ui.router.util', function($urlMatcherFactoryProvider) {
$urlMatcherFactoryProvider.type("myType", {}, function() {
return { decode: function() { return 'decoded'; }
};
return {
decode: function() { return { status: 'decoded' }; },
is: angular.isObject
};
});
m = new UrlMatcher("/test?{foo:myType}");
}));

it("should handle arrays properly with config-time custom type definitions", inject(function ($stateParams) {
expect(m.exec("/test", {foo: '1'})).toEqual({ foo: 'decoded' });
expect(m.exec("/test", {foo: ['1', '2']})).toEqual({ foo: ['decoded', 'decoded'] });
expect(m.exec("/test", {foo: '1'})).toEqual({ foo: { status: 'decoded' } });
expect(m.exec("/test", {foo: ['1', '2']})).toEqual({ foo: [ { status: 'decoded' }, { status: 'decoded' }] });
}));
});
});
Expand Down Expand Up @@ -662,7 +664,7 @@ describe("urlMatcherFactory", function () {

it("should populate query params", function() {
var defaults = { order: "name", limit: 25, page: 1 };
var m = new UrlMatcher('/foo?order&limit&page', {
var m = new UrlMatcher('/foo?order&{limit:int}&{page:int}', {
params: defaults
});
expect(m.exec("/foo")).toEqual(defaults);
Expand All @@ -687,7 +689,7 @@ describe("urlMatcherFactory", function () {
});

it("should allow injectable functions", inject(function($stateParams) {
var m = new UrlMatcher('/users/:user', {
var m = new UrlMatcher('/users/{user:json}', {
params: {
user: function($stateParams) {
return $stateParams.user;
Expand Down

0 comments on commit b0c6aa2

Please sign in to comment.