core-api/routing: implement parameterized and absolute path resolution

This commit is contained in:
Patrik Oldsberg
2020-09-19 14:56:27 +02:00
parent e52a6daae8
commit 558ae5593e
2 changed files with 74 additions and 27 deletions
@@ -14,13 +14,13 @@
* limitations under the License.
*/
import { RouteRefRegistry } from './RouteRefRegistry';
import { RouteRefRegistry, resolveRoute } from './RouteRefRegistry';
const ref1 = {};
const ref11 = {};
const ref12 = {};
const ref121 = {};
const ref2 = {};
const ref1 = { [resolveRoute]: (path: string) => path };
const ref11 = { [resolveRoute]: (path: string) => path };
const ref12 = { [resolveRoute]: (path: string) => path };
const ref121 = { [resolveRoute]: (path: string) => path };
const ref2 = { [resolveRoute]: (path: string) => path };
describe('RouteRefRegistry', () => {
it('should be constructed with a root route', () => {
@@ -41,18 +41,20 @@ describe('RouteRefRegistry', () => {
expect(registry.registerRoute([ref2], '2')).toBe(true);
expect(registry.registerRoute([ref2], 'duplicate')).toBe(false);
expect(registry.resolveRoute([], [ref1])).toBe('1');
expect(registry.resolveRoute([], [ref1])).toBe('/1');
expect(registry.resolveRoute([], [ref11])).toBe(undefined);
expect(registry.resolveRoute([], [ref1, ref11])).toBe('11');
expect(registry.resolveRoute([ref1], [ref11])).toBe('11');
expect(registry.resolveRoute([ref1], [ref2])).toBe('2');
expect(registry.resolveRoute([ref1, ref12, ref121], [])).toBe('121');
expect(registry.resolveRoute([ref1, ref12, ref121], [ref121])).toBe('121');
expect(registry.resolveRoute([ref1, ref12, ref121], [ref12, ref121])).toBe(
'121',
expect(registry.resolveRoute([], [ref1, ref11])).toBe('/1/11');
expect(registry.resolveRoute([ref1], [ref11])).toBe('/1/11');
expect(registry.resolveRoute([ref1], [ref2])).toBe('/2');
expect(registry.resolveRoute([ref1, ref12, ref121], [])).toBe('/1/12/121');
expect(registry.resolveRoute([ref1, ref12, ref121], [ref121])).toBe(
'/1/12/121',
);
expect(registry.resolveRoute([ref1, ref12, ref121], [ref12])).toBe('12');
expect(registry.resolveRoute([ref1, ref12, ref121], [ref1])).toBe('1');
expect(registry.resolveRoute([ref1, ref12, ref121], [ref12, ref121])).toBe(
'/1/12/121',
);
expect(registry.resolveRoute([ref1, ref12, ref121], [ref12])).toBe('/1/12');
expect(registry.resolveRoute([ref1, ref12, ref121], [ref1])).toBe('/1');
});
it('should throw when registering routes incorrectly', () => {
@@ -14,21 +14,30 @@
* limitations under the License.
*/
export const resolveRoute = Symbol('resolve-route');
type ConcreteRoute = {
[resolveRoute](path: string): string;
};
const rootRoute: ConcreteRoute = {
[resolveRoute]: () => '',
};
export type RouteRefResolver = {
resolveRoute(from: unknown[], to: unknown[]): string;
resolveRoute(from: ConcreteRoute[], to: ConcreteRoute[]): string;
};
class Node {
readonly children = new Map<unknown, Node>();
readonly children = new Map<ConcreteRoute, Node>();
constructor(readonly path: string, readonly parent: Node | undefined) {}
/**
* Look up a node in the tree given a path.
*/
findNode(routes: unknown[]): Node | undefined {
// eslint-disable-next-line consistent-this
let node: Node | undefined = this;
findNode(routes: ConcreteRoute[]): Node | undefined {
let node = this as Node | undefined;
for (let i = 0; i < routes.length; i++) {
node = node?.children.get(routes[i]);
@@ -43,7 +52,7 @@ class Node {
*
* Returns true if the node was added, or false if the node already existed.
*/
addNode(routes: unknown[], path: string): boolean {
addNode(routes: ConcreteRoute[], path: string): boolean {
if (routes.length === 0) {
throw new Error('Must provide at least 1 route to add routing node');
}
@@ -61,6 +70,35 @@ class Node {
parentNode.children.set(lastRoute, new Node(path, parentNode));
return true;
}
/**
* Resolve an absolute URL that represents this node in the routing tree, using
* using the supplied concrete routes and ancestors of this node.
*
* The length of the provided routes array must match the depth of
* the routing tree that this node is at, or an error will be thrown.
*/
resolve(routes: ConcreteRoute[]) {
const parts = Array(routes.length);
let node = this as Node | undefined;
for (let i = routes.length - 1; i >= 0; i--) {
if (!node) {
throw new Error('Route resolve missing required parent');
}
const route = routes[i];
parts[i] = route[resolveRoute](node.path);
node = node.parent;
}
if (node) {
throw new Error('Route resolve did not reach root');
}
return parts.join('/');
}
}
/**
@@ -73,12 +111,12 @@ export class RouteRefRegistry {
* Register a new leaf path for a sequence of routes. All ancestor
* routes must already exist.
*/
registerRoute(routes: unknown[], path: string): boolean {
registerRoute(routes: ConcreteRoute[], path: string): boolean {
return this.root.addNode(routes, path);
}
/**
* Resolve a route from a point in the routing tree.
* Resolve an absolute path from a point in the routing tree.
*
* The route referenced by `from` must exist, and is the starting
* point for the search, walking up the tree until a subtree that
@@ -87,14 +125,21 @@ export class RouteRefRegistry {
* If `from` is empty, the search starts and ends at the root node.
* If `to` is empty, the route referenced by `from` will always be returned.
*/
resolveRoute(from: unknown[], to: unknown[]): string | undefined {
let fromNode = this.root.findNode(from);
resolveRoute(from: ConcreteRoute[], to: ConcreteRoute[]): string | undefined {
// Keep track of the `from` routes and pop the last ones as we traverse up
// the routing tree. The list of concrete routes that we're passing to
// `node.resolve()` should only include the ones in the resolve path.
const concreteStack = from.slice();
let fromNode = this.root.findNode(from);
while (fromNode) {
const resolvedNode = fromNode.findNode(to);
if (resolvedNode) {
return resolvedNode.path;
return resolvedNode.resolve([rootRoute].concat(concreteStack, to));
}
// Search at this level of the tree failed, move up to parent
concreteStack.pop();
fromNode = fromNode.parent;
}