core-api: remove RouteRefRegistry and SubRouteRefs

Co-authored-by: blam <ben@blam.sh>
This commit is contained in:
Patrik Oldsberg
2020-11-30 13:53:46 +01:00
parent 4ef2a8769b
commit db28b524c4
5 changed files with 3 additions and 322 deletions
+2 -50
View File
@@ -14,43 +14,9 @@
* limitations under the License.
*/
import {
ConcreteRoute,
routeReference,
ReferencedRoute,
resolveRoute,
RouteRefConfig,
} from './types';
import { generatePath } from 'react-router-dom';
import { RouteRefConfig } from './types';
type SubRouteConfig = {
path: string;
};
export class SubRouteRef<T extends { [name in string]: string } | never = never>
implements ReferencedRoute {
constructor(
private readonly parent: ConcreteRoute,
private readonly config: SubRouteConfig,
) {}
get [routeReference]() {
return this;
}
link<Args extends T extends never ? [] : [T]>(...args: Args): ConcreteRoute {
return {
[routeReference]: this,
[resolveRoute]: (path: string) => {
const ownPart = generatePath(this.config.path, args[0] ?? {});
const parentPart = this.parent[resolveRoute](path);
return parentPart + ownPart;
},
};
}
}
export class AbsoluteRouteRef implements ConcreteRoute {
export class AbsoluteRouteRef {
constructor(private readonly config: RouteRefConfig) {}
get icon() {
@@ -65,20 +31,6 @@ export class AbsoluteRouteRef implements ConcreteRoute {
get title() {
return this.config.title;
}
createSubRoute<T extends { [name in string]: string } | never = never>(
config: SubRouteConfig,
) {
return new SubRouteRef<T>(this, config);
}
get [routeReference]() {
return this;
}
[resolveRoute](path: string) {
return path;
}
}
export function createRouteRef(config: RouteRefConfig): AbsoluteRouteRef {
@@ -1,105 +0,0 @@
/*
* 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';
import { createRouteRef } from './RouteRef';
const dummyConfig = { path: '/', icon: () => null, title: 'my-title' };
const ref1 = createRouteRef(dummyConfig);
const ref11 = createRouteRef(dummyConfig);
const ref12 = createRouteRef(dummyConfig);
const ref121 = createRouteRef(dummyConfig);
const ref2 = createRouteRef(dummyConfig);
const ref2a = ref2.createSubRoute({ path: '/a' });
const ref2b = ref2.createSubRoute<{ id: string }>({ path: '/b/:id' });
describe('RouteRefRegistry', () => {
it('should be constructed with a root route', () => {
const registry = new RouteRefRegistry();
expect(registry.resolveRoute([], [])).toBe('');
});
it('should register and resolve some absolute 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.registerRoute([ref2], '2')).toBe(true);
expect(registry.resolveRoute([], [ref1])).toBe('/1');
expect(registry.resolveRoute([], [ref11])).toBe(undefined);
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, 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 register and resolve with sub routes', () => {
const registry = new RouteRefRegistry();
expect(registry.registerRoute([ref1], '1')).toBe(true);
expect(registry.registerRoute([ref2], '2')).toBe(true);
expect(registry.registerRoute([ref2a], '2')).toBe(true);
expect(registry.registerRoute([ref2a, ref1], '1')).toBe(true);
expect(registry.registerRoute([ref2a, ref2], '2')).toBe(true);
expect(registry.registerRoute([ref2b], '2')).toBe(true);
expect(registry.registerRoute([ref2b, ref1], '1')).toBe(true);
expect(registry.registerRoute([ref2b, ref2], '2')).toBe(true);
expect(registry.resolveRoute([], [ref1])).toBe('/1');
expect(registry.resolveRoute([], [ref2])).toBe('/2');
expect(registry.resolveRoute([], [ref2a.link(), ref1])).toBe('/2/a/1');
expect(registry.resolveRoute([], [ref2a.link(), ref2])).toBe('/2/a/2');
expect(registry.resolveRoute([ref2a.link()], [ref2])).toBe('/2/a/2');
expect(registry.resolveRoute([ref2a.link(), ref1], [ref2])).toBe('/2/a/2');
expect(registry.resolveRoute([], [ref2b.link({ id: 'abc' }), ref1])).toBe(
'/2/b/abc/1',
);
expect(registry.resolveRoute([], [ref2b.link({ id: 'xyz' }), ref2])).toBe(
'/2/b/xyz/2',
);
expect(registry.resolveRoute([ref2b.link({ id: 'abc' })], [ref2])).toBe(
'/2/b/abc/2',
);
expect(
registry.resolveRoute([ref2b.link({ id: 'abc' }), ref1], [ref2]),
).toBe('/2/b/abc/2');
});
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');
});
});
@@ -1,155 +0,0 @@
/*
* 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 {
ConcreteRoute,
routeReference,
resolveRoute,
ReferencedRoute,
} from './types';
const rootRoute: ConcreteRoute = {
get [routeReference]() {
return this;
},
[resolveRoute]: () => '',
};
export type RouteRefResolver = {
resolveRoute(from: ConcreteRoute[], to: ConcreteRoute[]): 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: ReferencedRoute[]): Node | undefined {
let node = this as Node | undefined;
for (let i = 0; i < routes.length; i++) {
node = node?.children.get(routes[i][routeReference]);
}
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: ReferencedRoute[], 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];
const lastRouteRef = lastRoute[routeReference];
const existingNode = parentNode.children.get(lastRouteRef);
if (existingNode) {
return existingNode.path === path;
}
parentNode.children.set(lastRouteRef, 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('/');
}
}
/**
* 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: ReferencedRoute[], path: string): boolean {
return this.root.addNode(routes, path);
}
/**
* 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
* 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: 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.resolve([rootRoute].concat(concreteStack, to));
}
// Search at this level of the tree failed, move up to parent
concreteStack.pop();
fromNode = fromNode.parent;
}
return undefined;
}
}
+1 -1
View File
@@ -14,6 +14,6 @@
* limitations under the License.
*/
export type { RouteRef, RouteRefConfig, ConcreteRoute } from './types';
export type { RouteRef, RouteRefConfig } from './types';
export type { MutableRouteRef, AbsoluteRouteRef } from './RouteRef';
export { createRouteRef } from './RouteRef';
-11
View File
@@ -16,17 +16,6 @@
import { IconComponent } from '../icons';
export const resolveRoute = Symbol('resolve-route');
export const routeReference = Symbol('route-ref');
export type ReferencedRoute = {
[routeReference]: unknown;
};
export type ConcreteRoute = ReferencedRoute & {
[resolveRoute](path: string): string;
};
export type RouteRef = {
// TODO(Rugvip): Remove path, look up via registry instead
path: string;