core-api/routing: initial subroute implementation
This commit is contained in:
@@ -14,7 +14,36 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { ConcreteRoute, resolveRoute, RouteRefConfig } from './types';
|
||||
import { ConcreteRoute, ref, resolveRoute, RouteRefConfig } from './types';
|
||||
import { generatePath } from 'react-router-dom';
|
||||
|
||||
type SubRouteConfig = {
|
||||
path: string;
|
||||
};
|
||||
|
||||
export class SubRouteRef<T extends { [name in string]: string }> {
|
||||
constructor(
|
||||
private readonly parent: ConcreteRoute,
|
||||
private readonly config: SubRouteConfig,
|
||||
) {}
|
||||
|
||||
[ref]() {
|
||||
return this;
|
||||
}
|
||||
|
||||
link(params: T): ConcreteRoute {
|
||||
const selfRef = this as unknown;
|
||||
|
||||
return {
|
||||
[ref]: () => selfRef,
|
||||
[resolveRoute]: (path: string) => {
|
||||
const ownPart = generatePath(this.config.path, params);
|
||||
const parentPart = this.parent[resolveRoute](path);
|
||||
return parentPart + ownPart;
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export class AbsoluteRouteRef implements ConcreteRoute {
|
||||
constructor(private readonly config: RouteRefConfig) {}
|
||||
@@ -32,6 +61,16 @@ export class AbsoluteRouteRef implements ConcreteRoute {
|
||||
return this.config.title;
|
||||
}
|
||||
|
||||
createSubRoute<T extends { [name in string]: string }>(
|
||||
config: SubRouteConfig,
|
||||
) {
|
||||
return new SubRouteRef<T>(this, config);
|
||||
}
|
||||
|
||||
[ref]() {
|
||||
return this;
|
||||
}
|
||||
|
||||
[resolveRoute](path: string) {
|
||||
return path;
|
||||
}
|
||||
|
||||
@@ -17,12 +17,14 @@
|
||||
import { RouteRefRegistry } from './RouteRefRegistry';
|
||||
import { createRouteRef } from './RouteRef';
|
||||
|
||||
const dummyConfig = { path: '/', icon: null, title: 'my-title' };
|
||||
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', () => {
|
||||
@@ -30,7 +32,7 @@ describe('RouteRefRegistry', () => {
|
||||
expect(registry.resolveRoute([], [])).toBe('');
|
||||
});
|
||||
|
||||
it('should register and resolve some routes', () => {
|
||||
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);
|
||||
@@ -59,6 +61,39 @@ describe('RouteRefRegistry', () => {
|
||||
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(() => {
|
||||
|
||||
@@ -14,9 +14,12 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { ConcreteRoute, resolveRoute } from './types';
|
||||
import { ConcreteRoute, ref, resolveRoute, ReferencedRoute } from './types';
|
||||
|
||||
const rootRoute: ConcreteRoute = {
|
||||
[ref]() {
|
||||
return this;
|
||||
},
|
||||
[resolveRoute]: () => '',
|
||||
};
|
||||
|
||||
@@ -25,18 +28,18 @@ export type RouteRefResolver = {
|
||||
};
|
||||
|
||||
class Node {
|
||||
readonly children = new Map<ConcreteRoute, 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: ConcreteRoute[]): Node | undefined {
|
||||
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]);
|
||||
node = node?.children.get(routes[i][ref]());
|
||||
}
|
||||
|
||||
return node;
|
||||
@@ -48,7 +51,7 @@ class Node {
|
||||
*
|
||||
* Returns true if the node was added, or false if the node already existed.
|
||||
*/
|
||||
addNode(routes: ConcreteRoute[], path: string): boolean {
|
||||
addNode(routes: ReferencedRoute[], path: string): boolean {
|
||||
if (routes.length === 0) {
|
||||
throw new Error('Must provide at least 1 route to add routing node');
|
||||
}
|
||||
@@ -59,11 +62,12 @@ class Node {
|
||||
}
|
||||
|
||||
const lastRoute = routes[routes.length - 1];
|
||||
if (parentNode.children.has(lastRoute)) {
|
||||
const lastRouteRef = lastRoute[ref]();
|
||||
if (parentNode.children.has(lastRouteRef)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
parentNode.children.set(lastRoute, new Node(path, parentNode));
|
||||
parentNode.children.set(lastRouteRef, new Node(path, parentNode));
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -107,7 +111,7 @@ export class RouteRefRegistry {
|
||||
* Register a new leaf path for a sequence of routes. All ancestor
|
||||
* routes must already exist.
|
||||
*/
|
||||
registerRoute(routes: ConcreteRoute[], path: string): boolean {
|
||||
registerRoute(routes: ReferencedRoute[], path: string): boolean {
|
||||
return this.root.addNode(routes, path);
|
||||
}
|
||||
|
||||
|
||||
@@ -17,8 +17,13 @@
|
||||
import { IconComponent } from '../icons';
|
||||
|
||||
export const resolveRoute = Symbol('resolve-route');
|
||||
export const ref = Symbol('route-ref');
|
||||
|
||||
export type ConcreteRoute = {
|
||||
export type ReferencedRoute = {
|
||||
[ref](): unknown;
|
||||
};
|
||||
|
||||
export type ConcreteRoute = ReferencedRoute & {
|
||||
[resolveRoute](path: string): string;
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user