Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Create route methods implementation #31

Closed
wants to merge 9 commits into from
2 changes: 1 addition & 1 deletion src/types/resolved.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,6 @@ export type Resolved<T extends Route> = {
name: string,
path: string,
regex: RegExp,
parentNames: T['name'][],
parentNames: string[],
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lol curious what you ran into here. Guessing T['name'][] resolves to (string | undefined)[]?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

exactly. Originally I thought I needed to retain values from T but not sure it was actually providing any value

params: Record<string, Param[]>,
}
3 changes: 1 addition & 2 deletions src/types/routeMethods.test-d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,7 @@ import { test, expectTypeOf } from 'vitest'
import { ParamGetSet } from '@/types/params'
import { Routes } from '@/types/routes'
import { createRouter, path } from '@/utilities'

const component = { template: '<div>This is component</div>' }
import { component } from '@/utilities/testHelpers'

const boolean: ParamGetSet<boolean> = {
get: value => Boolean(value),
Expand Down
12 changes: 3 additions & 9 deletions src/types/routeMethods.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Param, ParamGetSet, ParamGetter } from '@/types/params'
import { Route, Routes } from '@/types/routes'
import { Public, Route, Routes } from '@/types/routes'
import { Identity, IsAny, IsEmptyObject, ReplaceAll, TupleCanBeAllUndefined, UnionToIntersection } from '@/types/utilities'
import { Path } from '@/utilities/path'

Expand Down Expand Up @@ -34,22 +34,16 @@ type RouteMethodsOrMethod<
TParams extends Record<string, unknown>
> = TRoute extends { path: infer Path, children: infer Children }
? Children extends Routes
? IsPublicRoute<TRoute> extends true
? TRoute extends Public<TRoute>
? RouteMethods<Children, MergeParams<TParams, ExtractParamsFromPath<Path>>> & CreateRouteMethod<TParams, Path>
: RouteMethods<Children, MergeParams<TParams, ExtractParamsFromPath<Path>>>
: never
: TRoute extends { path: infer Path }
? IsPublicRoute<TRoute> extends true
? TRoute extends Public<TRoute>
? CreateRouteMethod<TParams, Path>
: never
: never

type IsPublicRoute<TRoute extends Route> = 'public' extends keyof TRoute
? TRoute extends { public: false }
? false
: true
: true

type CreateRouteMethod<
TParams extends Record<string, unknown>,
TPath extends Route['path']
Expand Down
6 changes: 6 additions & 0 deletions src/types/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,4 +44,10 @@ export function isParentRoute(value: Route): value is ParentRoute {

export function isNamedRoute(value: Route): value is Route & { name: string } {
return 'name' in value && !!value.name
}

export type Public<T extends Route> = T & { public?: true | undefined }

export function isPublicRoute(value: Route): value is Public<Route> {
return value.public !== false
}
100 changes: 100 additions & 0 deletions src/utilities/createRouteMethods.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import { describe, expect, test } from 'vitest'
import { Routes } from '@/types'
import { createRouteMethods, resolveRoutes } from '@/utilities'
import { component } from '@/utilities/testHelpers'

describe('createRouteMethods', () => {
test.each([
[undefined],
[true],
Comment on lines +7 to +9
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doesn't look like you're using these values in your test.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed

])('given route is named and is public, makes parent callable', (isPublic) => {
const routes = [
{
name: 'parent',
path: '/parent',
public: isPublic,
component,
},
] as const satisfies Routes
const resolved = resolveRoutes(routes)

const response = createRouteMethods(resolved)

expect(response.parent).toBeTypeOf('function')
})

test('given route is NOT public, returns empty object', () => {
const routes = [
{
name: 'parent',
path: '/parent',
public: false,
component,
},
] as const satisfies Routes
const resolved = resolveRoutes(routes)

const response = createRouteMethods(resolved)

expect(response).toMatchObject({})
})

test.each([
[undefined],
[true],
[false],
])('given parent route with named children, has property for child name', (isPublic) => {
const routes = [
{
name: 'parent',
path: '/parent',
children: [
{
name: 'child',
path: '/child',
public: isPublic,
component,
},
],
},
] as const satisfies Routes
const resolved = resolveRoutes(routes)

const response = createRouteMethods(resolved)

expect(response.parent.child).toBeDefined()

if (isPublic !== false) {
expect(response.parent.child).toBeTypeOf('function')
}
})

test('given parent route with named children and grandchildren, has path to grandchild all callable', () => {
const routes = [
{
name: 'parent',
path: '/parent',
children: [
{
name: 'child',
path: '/child',
children: [
{
name: 'grandchild',
path: '/grandchild',
component,
},
],
},
],
},
] as const satisfies Routes
const resolved = resolveRoutes(routes)

const response = createRouteMethods(resolved)

expect(response.parent).toBeTypeOf('function')
expect(response.parent.child).toBeTypeOf('function')
expect(response.parent.child.grandchild).toBeTypeOf('function')
})
})
55 changes: 55 additions & 0 deletions src/utilities/createRouteMethods.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { Resolved, Route, RouteMethodRoute, isPublicRoute } from '@/types'
import { assembleUrl } from '@/utilities/urlAssembly'

type NonCallableNode = Record<string, any>
type CallableNode = NonCallableNode & {
(values: Record<string, unknown[]>): RouteMethodRoute,
}
type Node = CallableNode | NonCallableNode
Comment on lines +4 to +8
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm assuming these types will get replaced with some of the types in RouteMethods.ts yeah?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yep, this these types are super wide and intended to just prove that the logic is working. I'm hoping we can replace these with something much better this afternoon


export function createRouteMethods(routes: Resolved<Route>[]): Record<string, Node> {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We'll probably want to use generics here somehow. Because we already have a type for RouteMethods and it accepts Routes.

But I assume you're accepting Resolved here because in order to actually create a route method you need the resolved/flattened route.

const methods: Record<string, any> = {}

routes.forEach(route => {
const currentLevel = traverseParents(route, methods)
const routeNode = createNodeForRoute(route)

Object.assign(routeNode, currentLevel[route.name])

currentLevel[route.name] = routeNode
})

return methods
}

function traverseParents(route: Resolved<Route>, currentLevel: Record<string, any>): Record<string, any> {
route.parentNames.forEach(parentName => {
if (!currentLevel[parentName]) {
currentLevel[parentName] = {}
}

currentLevel = currentLevel[parentName]
})

return currentLevel
}

function createNodeForRoute(route: Resolved<Route>): Node {
if (isPublicRoute(route.matched)) {
return createCallableNode(route)
}

return {}
}

function createCallableNode(route: Resolved<Route>): CallableNode {
const node: CallableNode = (values) => {
return {
push: () => null,
replace: () => null,
url: assembleUrl(route, values),
}
}

return node
}
3 changes: 1 addition & 2 deletions src/utilities/createRouter.spec.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import { describe, expect, test } from 'vitest'
import { Routes } from '@/types'
import { createRouter } from '@/utilities'

const component = { template: '<div>This is component</div>' }
import { component } from '@/utilities/testHelpers'

describe('router.routeMatch', () => {
test('given path WITHOUT params, returns match', () => {
Expand Down
9 changes: 2 additions & 7 deletions src/utilities/createRouter.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Resolved, RouteMethods, Routes } from '@/types'
import { resolveRoutes, routeParamsAreValid } from '@/utilities'
import { createRouteMethods, resolveRoutes, routeParamsAreValid } from '@/utilities'

export type Router<
TRoutes extends Routes
Expand All @@ -13,11 +13,6 @@
routeMatch: (path: string) => Resolved<TRoutes[number]> | undefined,
}

function createRouteMethods<T extends Routes>(_routes: T): RouteMethods<T> {
// use assembleUrl(route, args) to generate string URL
return {} as any
}

export function createRouter<T extends Routes>(routes: T): Router<T> {
const resolved = resolveRoutes(routes)

Expand Down Expand Up @@ -46,7 +41,7 @@
}

const router = {
routes: createRouteMethods(routes),
routes: createRouteMethods(resolved),
push,
replace,
forward,
Expand All @@ -55,4 +50,4 @@
routeMatch,
}

return router

Check failure on line 53 in src/utilities/createRouter.ts

View workflow job for this annotation

GitHub Actions / Type Validation

Type '{ routes: Record<string, Node>; push: (url: string) => void; replace: (url: string) => void; forward: () => void; back: () => void; go: (number: number) => void; routeMatch: (path: string) => Resolved<...> | undefined; }' is not assignable to type 'Router<T>'.
Expand Down
6 changes: 4 additions & 2 deletions src/utilities/index.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
export * from './createRouteMethods'
export * from './createRouter'
export * from './mergeParams'
export * from './paramValidation'
export * from './params'
export * from './paramsFinder'
export * from './paramValidation'
export * from './path'
export * from './random'
export * from './routeResolver'
export * from './string'
export * from './urlAssembly'
3 changes: 1 addition & 2 deletions src/utilities/paramValidation.spec.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import { describe, expect, test } from 'vitest'
import { Route } from '@/types'
import { resolveRoutes, routeParamsAreValid, path } from '@/utilities'

const component = { template: '<div>This is component</div>' }
import { component } from '@/utilities/testHelpers'

describe('routeParamsAreValid', () => {
test('given route WITHOUT params, always return true', () => {
Expand Down
3 changes: 1 addition & 2 deletions src/utilities/routeResolver.spec.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import { describe, expect, test } from 'vitest'
import { Route, Routes } from '@/types'
import { resolveRoutes, generateRouteRegexPattern, path } from '@/utilities'

const component = { template: '<div>This is component</div>' }
import { component } from '@/utilities/testHelpers'

describe('resolveRoutes', () => {
test('always returns 1 record per named route', () => {
Expand Down
4 changes: 3 additions & 1 deletion src/utilities/random.ts → src/utilities/testHelpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,6 @@ export const random = {

return randomNumber
},
}
}

export const component = { template: '<div>This is component</div>' }
3 changes: 1 addition & 2 deletions src/utilities/urlAssembly.spec.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import { describe, expect, test } from 'vitest'
import { InvalidRouteParamValueError, Route } from '@/types'
import { path, resolveRoutes } from '@/utilities'
import { component } from '@/utilities/testHelpers'
import { assembleUrl } from '@/utilities/urlAssembly'

const component = { template: '<div>This is component</div>' }

describe('assembleUrl', () => {
test.each([
['/simple'],
Expand Down
Loading