From b0c6aa2350fdd3ce8483144774adc12f5a72b7e9 Mon Sep 17 00:00:00 2001 From: christopherthielen Date: Mon, 24 Nov 2014 09:47:02 -0600 Subject: [PATCH] fix($urlMatcherFactory): add Type.$normalize function - 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. --- src/urlMatcherFactory.js | 42 ++++++++++++++++++++++++----------- test/urlMatcherFactorySpec.js | 14 +++++++----- 2 files changed, 37 insertions(+), 19 deletions(-) diff --git a/src/urlMatcherFactory.js b/src/urlMatcherFactory.js index cc8e28752..fb414dd47 100644 --- a/src/urlMatcherFactory.js +++ b/src/urlMatcherFactory.js @@ -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.: @@ -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) { @@ -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); }; @@ -571,7 +579,7 @@ function $UrlMatcherFactory() { string: { encode: valToString, decode: valFromString, - is: regexpMatches, + is: function(val) { return typeof val === "string"}, pattern: /[^/]*/ }, int: { @@ -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; } /** @@ -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 + "}"; } @@ -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 }; diff --git a/test/urlMatcherFactorySpec.js b/test/urlMatcherFactorySpec.js index 3f45a2156..80950bbb7 100755 --- a/test/urlMatcherFactorySpec.js +++ b/test/urlMatcherFactorySpec.js @@ -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' }] }); })); }); }); @@ -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); @@ -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;