feat(regex_url_paths): add regex_group_names to handle consistency with serializers

By adding `regex_named_groups` to regex route configurations we can consistently map
regex matching groups to component parameters.

This should fix #7554.

Closes #7694
This commit is contained in:
Sebastian Hillig 2016-03-21 17:31:42 +01:00 committed by Misko Hevery
parent 1f7449ccf4
commit ce013a3dd9
7 changed files with 79 additions and 9 deletions

View File

@ -22,14 +22,17 @@ export abstract class AbstractRoute implements RouteDefinition {
useAsDefault: boolean; useAsDefault: boolean;
path: string; path: string;
regex: string; regex: string;
regex_group_names: string[];
serializer: RegexSerializer; serializer: RegexSerializer;
data: {[key: string]: any}; data: {[key: string]: any};
constructor({name, useAsDefault, path, regex, serializer, data}: RouteDefinition) { constructor({name, useAsDefault, path, regex, regex_group_names, serializer,
data}: RouteDefinition) {
this.name = name; this.name = name;
this.useAsDefault = useAsDefault; this.useAsDefault = useAsDefault;
this.path = path; this.path = path;
this.regex = regex; this.regex = regex;
this.regex_group_names = regex_group_names;
this.serializer = serializer; this.serializer = serializer;
this.data = data; this.data = data;
} }
@ -62,12 +65,14 @@ export class Route extends AbstractRoute {
component: any; component: any;
aux: string = null; aux: string = null;
constructor({name, useAsDefault, path, regex, serializer, data, component}: RouteDefinition) { constructor({name, useAsDefault, path, regex, regex_group_names, serializer, data,
component}: RouteDefinition) {
super({ super({
name: name, name: name,
useAsDefault: useAsDefault, useAsDefault: useAsDefault,
path: path, path: path,
regex: regex, regex: regex,
regex_group_names: regex_group_names,
serializer: serializer, serializer: serializer,
data: data data: data
}); });
@ -99,12 +104,14 @@ export class Route extends AbstractRoute {
export class AuxRoute extends AbstractRoute { export class AuxRoute extends AbstractRoute {
component: any; component: any;
constructor({name, useAsDefault, path, regex, serializer, data, component}: RouteDefinition) { constructor({name, useAsDefault, path, regex, regex_group_names, serializer, data,
component}: RouteDefinition) {
super({ super({
name: name, name: name,
useAsDefault: useAsDefault, useAsDefault: useAsDefault,
path: path, path: path,
regex: regex, regex: regex,
regex_group_names: regex_group_names,
serializer: serializer, serializer: serializer,
data: data data: data
}); });
@ -141,12 +148,14 @@ export class AsyncRoute extends AbstractRoute {
loader: () => Promise<Type>; loader: () => Promise<Type>;
aux: string = null; aux: string = null;
constructor({name, useAsDefault, path, regex, serializer, data, loader}: RouteDefinition) { constructor({name, useAsDefault, path, regex, regex_group_names, serializer, data,
loader}: RouteDefinition) {
super({ super({
name: name, name: name,
useAsDefault: useAsDefault, useAsDefault: useAsDefault,
path: path, path: path,
regex: regex, regex: regex,
regex_group_names: regex_group_names,
serializer: serializer, serializer: serializer,
data: data data: data
}); });
@ -179,12 +188,14 @@ export class AsyncRoute extends AbstractRoute {
export class Redirect extends AbstractRoute { export class Redirect extends AbstractRoute {
redirectTo: any[]; redirectTo: any[];
constructor({name, useAsDefault, path, regex, serializer, data, redirectTo}: RouteDefinition) { constructor({name, useAsDefault, path, regex, regex_group_names, serializer, data,
redirectTo}: RouteDefinition) {
super({ super({
name: name, name: name,
useAsDefault: useAsDefault, useAsDefault: useAsDefault,
path: path, path: path,
regex: regex, regex: regex,
regex_group_names: regex_group_names,
serializer: serializer, serializer: serializer,
data: data data: data
}); });

View File

@ -5,6 +5,7 @@ abstract class RouteDefinition {
final String name; final String name;
final bool useAsDefault; final bool useAsDefault;
final String regex; final String regex;
final List<String> regex_group_names;
final Function serializer; final Function serializer;
const RouteDefinition({this.path, this.name, this.useAsDefault : false, this.regex, this.serializer}); const RouteDefinition({this.path, this.name, this.useAsDefault : false, this.regex, this.regex_group_names, this.serializer});
} }

View File

@ -16,6 +16,7 @@ export interface RouteDefinition {
path?: string; path?: string;
aux?: string; aux?: string;
regex?: string; regex?: string;
regex_group_names?: string[];
serializer?: RegexSerializer; serializer?: RegexSerializer;
component?: Type | ComponentDefinition; component?: Type | ComponentDefinition;
loader?: () => Promise<Type>; loader?: () => Promise<Type>;

View File

@ -1,10 +1,21 @@
import {RegExpWrapper, RegExpMatcherWrapper, isBlank} from '../../../src/facade/lang'; import {RegExpWrapper, RegExpMatcherWrapper, isBlank} from '../../../src/facade/lang';
import {BaseException} from '@angular/core';
import {Url} from '../../url_parser'; import {Url} from '../../url_parser';
import {RoutePath, GeneratedUrl, MatchedUrl} from './route_path'; import {RoutePath, GeneratedUrl, MatchedUrl} from './route_path';
export interface RegexSerializer { (params: {[key: string]: any}): GeneratedUrl; } export interface RegexSerializer { (params: {[key: string]: any}): GeneratedUrl; }
function computeNumberOfRegexGroups(regex: string): number {
// cleverly compute regex groups by appending an alternative empty matching
// pattern and match against an empty string, the resulting match still
// receives all the other groups
var test_regex = RegExpWrapper.create(regex + "|");
var matcher = RegExpWrapper.matcher(test_regex, '');
var match = RegExpMatcherWrapper.next(matcher);
return match.length;
}
export class RegexRoutePath implements RoutePath { export class RegexRoutePath implements RoutePath {
public hash: string; public hash: string;
public terminal: boolean = true; public terminal: boolean = true;
@ -12,9 +23,19 @@ export class RegexRoutePath implements RoutePath {
private _regex: RegExp; private _regex: RegExp;
constructor(private _reString: string, private _serializer: RegexSerializer) { constructor(private _reString: string, private _serializer: RegexSerializer,
private _groupNames?: Array<string>) {
this.hash = this._reString; this.hash = this._reString;
this._regex = RegExpWrapper.create(this._reString); this._regex = RegExpWrapper.create(this._reString);
if (this._groupNames != null) {
var groups = computeNumberOfRegexGroups(this._reString);
if (groups != _groupNames.length) {
throw new BaseException(
`Regex group names [${this._groupNames.join(',')}] must contain names for \
each matching group and a name for the complete match as its first element of regex \
'${this._reString}'. ${groups} group names are expected.`);
}
}
} }
matchUrl(url: Url): MatchedUrl { matchUrl(url: Url): MatchedUrl {
@ -28,7 +49,7 @@ export class RegexRoutePath implements RoutePath {
} }
for (let i = 0; i < match.length; i += 1) { for (let i = 0; i < match.length; i += 1) {
params[i.toString()] = match[i]; params[this._groupNames != null ? this._groupNames[i] : i.toString()] = match[i];
} }
return new MatchedUrl(urlPath, [], params, [], null); return new MatchedUrl(urlPath, [], params, [], null);

View File

@ -169,7 +169,7 @@ export class RuleSet {
private _getRoutePath(config: RouteDefinition): RoutePath { private _getRoutePath(config: RouteDefinition): RoutePath {
if (isPresent(config.regex)) { if (isPresent(config.regex)) {
if (isFunction(config.serializer)) { if (isFunction(config.serializer)) {
return new RegexRoutePath(config.regex, config.serializer); return new RegexRoutePath(config.regex, config.serializer, config.regex_group_names);
} else { } else {
throw new BaseException( throw new BaseException(
`Route provides a regex property, '${config.regex}', but no serializer property`); `Route provides a regex property, '${config.regex}', but no serializer property`);

View File

@ -45,5 +45,21 @@ export function main() {
var url = rec.generateUrl(params); var url = rec.generateUrl(params);
expect(url.urlPath).toEqual('/a/one/b/two'); expect(url.urlPath).toEqual('/a/one/b/two');
}); });
it('should raise an error when the number of parameters doesnt match', () => {
expect(() => {new RegexRoutePath('^a-([0-9]+)-b-([0-9]+)$', emptySerializer,
['complete_match', 'a'])})
.toThrowError(`Regex group names [complete_match,a] must contain names for each matching \
group and a name for the complete match as its first element of regex '^a-([0-9]+)-b-([0-9]+)$'. \
3 group names are expected.`);
});
it('should take group naming into account when passing params', () => {
var rec = new RegexRoutePath('^a-([0-9]+)-b-([0-9]+)$', emptySerializer,
['complete_match', 'a', 'b']);
var url = parser.parse('a-123-b-345');
var match = rec.matchUrl(url);
expect(match.allParams).toEqual({'complete_match': 'a-123-b-345', 'a': '123', 'b': '345'});
});
}); });
} }

View File

@ -83,6 +83,26 @@ export function main() {
}); });
})); }));
it('should recognize a regex with named_groups', inject([AsyncTestCompleter], (async) => {
function emptySerializer(params): GeneratedUrl { return new GeneratedUrl('', {}); }
recognizer.config(new Route({
regex: '^(.+)/(.+)$',
regex_group_names: ['cc', 'a', 'b'],
serializer: emptySerializer,
component: DummyCmpA
}));
recognize(recognizer, '/first/second')
.then((solutions: RouteMatch[]) => {
expect(solutions.length).toBe(1);
expect(getComponentType(solutions[0])).toEqual(DummyCmpA);
expect(getParams(solutions[0]))
.toEqual({'cc': 'first/second', 'a': 'first', 'b': 'second'});
async.done();
});
}));
it('should throw when given two routes that start with the same static segment', () => { it('should throw when given two routes that start with the same static segment', () => {
recognizer.config(new Route({path: '/hello', component: DummyCmpA})); recognizer.config(new Route({path: '/hello', component: DummyCmpA}));