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:
parent
1f7449ccf4
commit
ce013a3dd9
@ -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
|
||||||
});
|
});
|
||||||
|
@ -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});
|
||||||
}
|
}
|
||||||
|
@ -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>;
|
||||||
|
@ -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);
|
||||||
|
@ -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`);
|
||||||
|
@ -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'});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -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}));
|
||||||
|
Loading…
x
Reference in New Issue
Block a user