From 0f3e9b29d44e1add0693a79abdcdbed11051be96 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sat, 19 Sep 2020 16:32:41 +0200 Subject: [PATCH] core-api/routing: initial subroute implementation --- packages/core-api/src/routing/RouteRef.ts | 41 ++++++++++++++++++- .../src/routing/RouteRefRegistry.test.ts | 39 +++++++++++++++++- .../core-api/src/routing/RouteRefRegistry.ts | 20 +++++---- packages/core-api/src/routing/types.ts | 7 +++- 4 files changed, 95 insertions(+), 12 deletions(-) diff --git a/packages/core-api/src/routing/RouteRef.ts b/packages/core-api/src/routing/RouteRef.ts index cc36446f8b..51bb9ee1a4 100644 --- a/packages/core-api/src/routing/RouteRef.ts +++ b/packages/core-api/src/routing/RouteRef.ts @@ -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 { + 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( + config: SubRouteConfig, + ) { + return new SubRouteRef(this, config); + } + + [ref]() { + return this; + } + [resolveRoute](path: string) { return path; } diff --git a/packages/core-api/src/routing/RouteRefRegistry.test.ts b/packages/core-api/src/routing/RouteRefRegistry.test.ts index 91bbc70117..2849bcf32b 100644 --- a/packages/core-api/src/routing/RouteRefRegistry.test.ts +++ b/packages/core-api/src/routing/RouteRefRegistry.test.ts @@ -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(() => { diff --git a/packages/core-api/src/routing/RouteRefRegistry.ts b/packages/core-api/src/routing/RouteRefRegistry.ts index 3b9bf4a7a9..7377a90672 100644 --- a/packages/core-api/src/routing/RouteRefRegistry.ts +++ b/packages/core-api/src/routing/RouteRefRegistry.ts @@ -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(); + readonly children = new Map(); 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); } diff --git a/packages/core-api/src/routing/types.ts b/packages/core-api/src/routing/types.ts index 9e5e2bb3a7..291029c3f2 100644 --- a/packages/core-api/src/routing/types.ts +++ b/packages/core-api/src/routing/types.ts @@ -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; };