Skip to content

Commit 4465f76

Browse files
committed
refactor(router): Extract common functions for config matching (angular#40029)
The `applyRedirects` and `recognize` algorithms have the same overall goal: match a `UrlTree` with the application's `Routes` config. There are a few key functions in these algorithms which can be shared rather than duplicated between the two. This also makes it easier to see how the two are similar and where they diverge. PR Close angular#40029
1 parent 68f0858 commit 4465f76

4 files changed

Lines changed: 252 additions & 284 deletions

File tree

‎packages/router/src/apply_redirects.ts‎

Lines changed: 43 additions & 112 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,11 @@ import {LoadedRouterConfig, Route, Routes} from './config';
1414
import {CanLoadFn} from './interfaces';
1515
import {prioritizedGuardValue} from './operators/prioritized_guard_value';
1616
import {RouterConfigLoader} from './router_config_loader';
17-
import {defaultUrlMatcher, navigationCancelingError, Params, PRIMARY_OUTLET} from './shared';
17+
import {navigationCancelingError, Params, PRIMARY_OUTLET} from './shared';
1818
import {UrlSegment, UrlSegmentGroup, UrlSerializer, UrlTree} from './url_tree';
1919
import {forEach, waitForMap, wrapIntoObservable} from './utils/collection';
2020
import {getOutlet, groupRoutesByOutlet} from './utils/config';
21+
import {match, noLeftoversInUrl, split} from './utils/config_matching';
2122
import {isCanLoad, isFunction, isUrlTree} from './utils/type_guards';
2223

2324
class NoMatch {
@@ -79,9 +80,10 @@ class ApplyRedirects {
7980
apply(): Observable<UrlTree> {
8081
const expanded$ =
8182
this.expandSegmentGroup(this.ngModule, this.config, this.urlTree.root, PRIMARY_OUTLET);
82-
const urlTrees$ = expanded$.pipe(
83-
map((rootSegmentGroup: UrlSegmentGroup) => this.createUrlTree(
84-
rootSegmentGroup, this.urlTree.queryParams, this.urlTree.fragment!)));
83+
const urlTrees$ = expanded$.pipe(map((rootSegmentGroup: UrlSegmentGroup) => {
84+
return this.createUrlTree(
85+
squashSegmentGroup(rootSegmentGroup), this.urlTree.queryParams, this.urlTree.fragment!);
86+
}));
8587
return urlTrees$.pipe(catchError((e: any) => {
8688
if (e instanceof AbsoluteRedirect) {
8789
// after an absolute redirect we do not apply any more redirects!
@@ -101,9 +103,10 @@ class ApplyRedirects {
101103
private match(tree: UrlTree): Observable<UrlTree> {
102104
const expanded$ =
103105
this.expandSegmentGroup(this.ngModule, this.config, tree.root, PRIMARY_OUTLET);
104-
const mapped$ = expanded$.pipe(
105-
map((rootSegmentGroup: UrlSegmentGroup) =>
106-
this.createUrlTree(rootSegmentGroup, tree.queryParams, tree.fragment!)));
106+
const mapped$ = expanded$.pipe(map((rootSegmentGroup: UrlSegmentGroup) => {
107+
return this.createUrlTree(
108+
squashSegmentGroup(rootSegmentGroup), tree.queryParams, tree.fragment!);
109+
}));
107110
return mapped$.pipe(catchError((e: any): Observable<UrlTree> => {
108111
if (e instanceof NoMatch) {
109112
throw this.noMatchError(e);
@@ -172,7 +175,7 @@ class ApplyRedirects {
172175
first((s: UrlSegmentGroup|null): s is UrlSegmentGroup => s !== null),
173176
catchError(e => {
174177
if (e instanceof EmptyError || e.name === 'EmptyError') {
175-
if (this.noLeftoversInUrl(segmentGroup, segments, outlet)) {
178+
if (noLeftoversInUrl(segmentGroup, segments, outlet)) {
176179
return of(new UrlSegmentGroup([], {}));
177180
}
178181
throw new NoMatch(segmentGroup);
@@ -197,11 +200,6 @@ class ApplyRedirects {
197200
);
198201
}
199202

200-
private noLeftoversInUrl(segmentGroup: UrlSegmentGroup, segments: UrlSegment[], outlet: string):
201-
boolean {
202-
return segments.length === 0 && !segmentGroup.children[outlet];
203-
}
204-
205203
private expandSegmentAgainstRoute(
206204
ngModule: NgModuleRef<any>, segmentGroup: UrlSegmentGroup, routes: Route[], route: Route,
207205
paths: UrlSegment[], outlet: string, allowRedirects: boolean): Observable<UrlSegmentGroup> {
@@ -256,8 +254,8 @@ class ApplyRedirects {
256254
match(segmentGroup, route, segments);
257255
if (!matched) return noMatch(segmentGroup);
258256

259-
const newTree = this.applyRedirectCommands(
260-
consumedSegments, route.redirectTo!, <any>positionalParamSegments);
257+
const newTree =
258+
this.applyRedirectCommands(consumedSegments, route.redirectTo!, positionalParamSegments);
261259
if (route.redirectTo!.startsWith('/')) {
262260
return absoluteRedirect(newTree);
263261
}
@@ -296,6 +294,14 @@ class ApplyRedirects {
296294

297295
const {segmentGroup, slicedSegments} =
298296
split(rawSegmentGroup, consumedSegments, rawSlicedSegments, childConfig);
297+
// TODO(atscott): clearing the source segment and segment index shift is only necessary to
298+
// prevent failures in tests which assert exact object matches. The `split` is now shared
299+
// between applyRedirects and recognize and only the `recognize` step needs these properties.
300+
// Before the implementations were merged, the applyRedirects would not assign them.
301+
// We should be able to remove this logic as a "breaking change" but should do some more
302+
// investigation into the failures first.
303+
segmentGroup._sourceSegment = undefined;
304+
segmentGroup._segmentIndexShift = undefined;
299305

300306
if (slicedSegments.length === 0 && segmentGroup.hasChildren()) {
301307
const expanded$ = this.expandChildren(childModule, childConfig, segmentGroup);
@@ -467,64 +473,15 @@ class ApplyRedirects {
467473
}
468474
}
469475

470-
function match(segmentGroup: UrlSegmentGroup, route: Route, segments: UrlSegment[]): {
471-
matched: boolean,
472-
consumedSegments: UrlSegment[],
473-
lastChild: number,
474-
positionalParamSegments: {[k: string]: UrlSegment}
475-
} {
476-
if (route.path === '') {
477-
if ((route.pathMatch === 'full') && (segmentGroup.hasChildren() || segments.length > 0)) {
478-
return {matched: false, consumedSegments: [], lastChild: 0, positionalParamSegments: {}};
479-
}
480-
481-
return {matched: true, consumedSegments: [], lastChild: 0, positionalParamSegments: {}};
482-
}
483-
484-
const matcher = route.matcher || defaultUrlMatcher;
485-
const res = matcher(segments, segmentGroup, route);
486-
487-
if (!res) {
488-
return {
489-
matched: false,
490-
consumedSegments: <any[]>[],
491-
lastChild: 0,
492-
positionalParamSegments: {},
493-
};
494-
}
495-
496-
return {
497-
matched: true,
498-
consumedSegments: res.consumed!,
499-
lastChild: res.consumed.length!,
500-
positionalParamSegments: res.posParams!,
501-
};
502-
}
503-
504-
function split(
505-
segmentGroup: UrlSegmentGroup, consumedSegments: UrlSegment[], slicedSegments: UrlSegment[],
506-
config: Route[]) {
507-
if (slicedSegments.length > 0 &&
508-
containsEmptyPathRedirectsWithNamedOutlets(segmentGroup, slicedSegments, config)) {
509-
const s = new UrlSegmentGroup(
510-
consumedSegments,
511-
createChildrenForEmptySegments(
512-
config, new UrlSegmentGroup(slicedSegments, segmentGroup.children)));
513-
return {segmentGroup: mergeTrivialChildren(s), slicedSegments: []};
514-
}
515-
516-
if (slicedSegments.length === 0 &&
517-
containsEmptyPathRedirects(segmentGroup, slicedSegments, config)) {
518-
const s = new UrlSegmentGroup(
519-
segmentGroup.segments,
520-
addEmptySegmentsToChildrenIfNeeded(
521-
segmentGroup, slicedSegments, config, segmentGroup.children));
522-
return {segmentGroup: mergeTrivialChildren(s), slicedSegments};
523-
}
524-
525-
return {segmentGroup, slicedSegments};
526-
}
527476

477+
/**
478+
* When possible, merges the primary outlet child into the parent `UrlSegmentGroup`.
479+
*
480+
* When a segment group has only one child which is a primary outlet, merges that child into the
481+
* parent. That is, the child segment group's segments are merged into the `s` and the child's
482+
* children become the children of `s`. Think of this like a 'squash', merging the child segment
483+
* group into the parent.
484+
*/
528485
function mergeTrivialChildren(s: UrlSegmentGroup): UrlSegmentGroup {
529486
if (s.numberOfChildren === 1 && s.children[PRIMARY_OUTLET]) {
530487
const c = s.children[PRIMARY_OUTLET];
@@ -534,46 +491,20 @@ function mergeTrivialChildren(s: UrlSegmentGroup): UrlSegmentGroup {
534491
return s;
535492
}
536493

537-
function addEmptySegmentsToChildrenIfNeeded(
538-
segmentGroup: UrlSegmentGroup, slicedSegments: UrlSegment[], routes: Route[],
539-
children: {[name: string]: UrlSegmentGroup}): {[name: string]: UrlSegmentGroup} {
540-
const res: {[name: string]: UrlSegmentGroup} = {};
541-
for (const r of routes) {
542-
if (isEmptyPathRedirect(segmentGroup, slicedSegments, r) && !children[getOutlet(r)]) {
543-
res[getOutlet(r)] = new UrlSegmentGroup([], {});
544-
}
545-
}
546-
return {...children, ...res};
547-
}
548-
549-
function createChildrenForEmptySegments(
550-
routes: Route[], primarySegmentGroup: UrlSegmentGroup): {[name: string]: UrlSegmentGroup} {
551-
const res: {[name: string]: UrlSegmentGroup} = {};
552-
res[PRIMARY_OUTLET] = primarySegmentGroup;
553-
for (const r of routes) {
554-
if (r.path === '' && getOutlet(r) !== PRIMARY_OUTLET) {
555-
res[getOutlet(r)] = new UrlSegmentGroup([], {});
494+
/**
495+
* Recursively merges primary segment children into their parents and also drops empty children
496+
* (those which have no segments and no children themselves). The latter prevents serializing a
497+
* group into something like `/a(aux:)`, where `aux` is an empty child segment.
498+
*/
499+
function squashSegmentGroup(segmentGroup: UrlSegmentGroup): UrlSegmentGroup {
500+
const newChildren = {} as any;
501+
for (const [childOutlet, child] of Object.entries(segmentGroup.children)) {
502+
const childCandidate = squashSegmentGroup(child);
503+
// don't add empty children
504+
if (childCandidate.segments.length > 0 || childCandidate.hasChildren()) {
505+
newChildren[childOutlet] = childCandidate;
556506
}
557507
}
558-
return res;
559-
}
560-
561-
function containsEmptyPathRedirectsWithNamedOutlets(
562-
segmentGroup: UrlSegmentGroup, segments: UrlSegment[], routes: Route[]): boolean {
563-
return routes.some(
564-
r => isEmptyPathRedirect(segmentGroup, segments, r) && getOutlet(r) !== PRIMARY_OUTLET);
565-
}
566-
567-
function containsEmptyPathRedirects(
568-
segmentGroup: UrlSegmentGroup, segments: UrlSegment[], routes: Route[]): boolean {
569-
return routes.some(r => isEmptyPathRedirect(segmentGroup, segments, r));
570-
}
571-
572-
function isEmptyPathRedirect(
573-
segmentGroup: UrlSegmentGroup, segments: UrlSegment[], r: Route): boolean {
574-
if ((segmentGroup.hasChildren() || segments.length > 0) && r.pathMatch === 'full') {
575-
return false;
576-
}
577-
578-
return r.path === '' && r.redirectTo !== undefined;
508+
const s = new UrlSegmentGroup(segmentGroup.segments, newChildren);
509+
return mergeTrivialChildren(s);
579510
}

0 commit comments

Comments
 (0)