core-api/routing: initial RouteRefRegistry implementation

This commit is contained in:
Patrik Oldsberg
2020-09-19 13:17:08 +02:00
parent 8b53edb474
commit e52a6daae8
2 changed files with 170 additions and 0 deletions
@@ -0,0 +1,67 @@
/*
* Copyright 2020 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { RouteRefRegistry } from './RouteRefRegistry';
const ref1 = {};
const ref11 = {};
const ref12 = {};
const ref121 = {};
const ref2 = {};
describe('RouteRefRegistry', () => {
it('should be constructed with a root route', () => {
const registry = new RouteRefRegistry();
expect(registry.resolveRoute([], [])).toBe('');
});
it('should register and resolve some routes', () => {
const registry = new RouteRefRegistry();
expect(registry.registerRoute([ref1], '1')).toBe(true);
expect(registry.registerRoute([ref1, ref11], '11')).toBe(true);
expect(registry.registerRoute([ref1, ref12], '12')).toBe(true);
expect(registry.registerRoute([ref1, ref12, ref121], '121')).toBe(true);
expect(registry.registerRoute([ref1, ref12, ref121], 'duplicate')).toBe(
false,
);
expect(registry.registerRoute([ref1, ref12], 'duplicate')).toBe(false);
expect(registry.registerRoute([ref2], '2')).toBe(true);
expect(registry.registerRoute([ref2], 'duplicate')).toBe(false);
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, ref12, ref121], [ref12])).toBe('12');
expect(registry.resolveRoute([ref1, ref12, ref121], [ref1])).toBe('1');
});
it('should throw when registering routes incorrectly', () => {
const registry = new RouteRefRegistry();
expect(() => {
registry.registerRoute([ref1, ref11], '11');
}).toThrow('Could not find parent for new routing node');
expect(() => {
registry.registerRoute([], '11');
}).toThrow('Must provide at least 1 route to add routing node');
});
});
@@ -0,0 +1,103 @@
/*
* Copyright 2020 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export type RouteRefResolver = {
resolveRoute(from: unknown[], to: unknown[]): string;
};
class Node {
readonly children = new Map<unknown, 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;
for (let i = 0; i < routes.length; i++) {
node = node?.children.get(routes[i]);
}
return node;
}
/**
* Assigns a path to a leaf node in the routing tree. All ancestor
* nodes of the new leaf node must already exist, or an error will be thrown.
*
* Returns true if the node was added, or false if the node already existed.
*/
addNode(routes: unknown[], path: string): boolean {
if (routes.length === 0) {
throw new Error('Must provide at least 1 route to add routing node');
}
const parentNode = this.findNode(routes.slice(0, -1));
if (!parentNode) {
throw new Error('Could not find parent for new routing node');
}
const lastRoute = routes[routes.length - 1];
if (parentNode.children.has(lastRoute)) {
return false;
}
parentNode.children.set(lastRoute, new Node(path, parentNode));
return true;
}
}
/**
* A registry for resolving route refs into concrete string routes.
*/
export class RouteRefRegistry {
private readonly root = new Node('', undefined);
/**
* Register a new leaf path for a sequence of routes. All ancestor
* routes must already exist.
*/
registerRoute(routes: unknown[], path: string): boolean {
return this.root.addNode(routes, path);
}
/**
* Resolve a route 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
* matches the routes reference in `to` are found.
*
* 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);
while (fromNode) {
const resolvedNode = fromNode.findNode(to);
if (resolvedNode) {
return resolvedNode.path;
}
fromNode = fromNode.parent;
}
return undefined;
}
}