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

merge parent route meta with children route meta #242

Merged
merged 3 commits into from
Aug 18, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions src/errors/metaPropertyConflict.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
/**
* An error thrown when a parent's meta has the same key as a child and the types are not compatible.
* A child's meta can override properties of the parent, however the types must match!
*/
export class MetaPropertyConflict extends Error {
public constructor(property?: string) {
super(`Child property on meta for ${property} conflicts with the parent meta.`)
}
}
35 changes: 35 additions & 0 deletions src/services/combineMeta.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { expect, test } from 'vitest'
import { MetaPropertyConflict } from '@/errors/metaPropertyConflict'
import { combineMeta } from '@/services/combineMeta'

test('given 2 meta objects, returns new meta joined together', () => {
const aMeta = { foz: 123 }
const bMeta = { baz: 'baz' }

const response = combineMeta(aMeta, bMeta)

expect(response).toMatchObject({
foz: 123,
baz: 'baz',
})
})

test('given 2 meta objects with duplicate properties but same type, overrides parent value with child', () => {
const aMeta = { foz: 123 }
const bMeta = { foz: 456 }

const response = combineMeta(aMeta, bMeta)

expect(response).toMatchObject({
foz: 456,
})
})

test('given 2 meta objects with duplicate properties with DIFFERENT types, throws MetaPropertyConflict error', () => {
const aMeta = { foz: 123 }
const bMeta = { foz: 'baz' }

const action: () => void = () => combineMeta(aMeta, bMeta)

expect(action).toThrow(MetaPropertyConflict)
})
23 changes: 23 additions & 0 deletions src/services/combineMeta.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { MetaPropertyConflict } from '@/errors/metaPropertyConflict'

export type CombineMeta<
TParent extends Record<string, unknown>,
TChild extends Record<string, unknown>
> = TParent & TChild

export function combineMeta<TParentMeta extends Record<string, unknown>, TChildMeta extends Record<string, unknown>>(parentMeta: TParentMeta, childMeta: TChildMeta): CombineMeta<TParentMeta, TChildMeta>
export function combineMeta(parentMeta: Record<string, unknown>, childMeta: Record<string, unknown>): Record<string, unknown> {
checkForConflicts(parentMeta, childMeta)

return { ...parentMeta, ...childMeta }
}

function checkForConflicts(parentMeta: Record<string, unknown>, childMeta: Record<string, unknown>): void {
const conflict = Object.keys(parentMeta).find(key => {
return key in childMeta && typeof childMeta[key] !== typeof parentMeta[key]
})

if (conflict) {
throw new MetaPropertyConflict(conflict)
}
}
2 changes: 2 additions & 0 deletions src/services/createExternalRoute.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ export function createExternalRoute(options: CreateRouteOptions): Route {
const key = toKey(options.name)
const path = toPath(options.path)
const query = toQuery(options.query)
const meta = options.meta ?? {}
const host = isWithHost(options) ? toHost(options.host) : toHost('')
const rawRoute = markRaw({ meta: {}, state: {}, ...options })

Expand All @@ -38,6 +39,7 @@ export function createExternalRoute(options: CreateRouteOptions): Route {
host,
path,
query,
meta,
depth: 1,
stateParams: {},
}
Expand Down
125 changes: 125 additions & 0 deletions src/services/createRoute.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
import { expect, test } from 'vitest'
import { createRoute } from '@/services/createRoute'
import { path } from '@/services/path'
import { query } from '@/services/query'

test('given parent, key is combined', () => {
const parent = createRoute({
name: 'parent',
})

const child = createRoute({
parent: parent,
name: 'child',
})

expect(child.key).toBe('parent.child')
})

test('given parent, path is combined', () => {
const parent = createRoute({
path: '/parent',
})

const child = createRoute({
parent: parent,
path: path('/child/[id]', { id: Number }),
})

expect(child.path).toMatchObject({
path: '/parent/child/[id]',
params: {
id: Number,
},
})
})

test('given parent, query is combined', () => {
const parent = createRoute({
query: 'static=123',
})

const child = createRoute({
parent: parent,
query: query('sort=[sort]', { sort: Boolean }),
})

expect(child.query).toMatchObject({
query: 'static=123&sort=[sort]',
params: {
sort: Boolean,
},
})
})

test('given parent, state is combined into stateParams', () => {
stackoverfloweth marked this conversation as resolved.
Show resolved Hide resolved
const parent = createRoute({
state: {
foo: Number,
},
})

const child = createRoute({
parent: parent,
state: {
bar: String,
},
})

expect(child.stateParams).toMatchObject({
foo: Number,
bar: String,
})
})

test('given parent and child without state, state matches parent', () => {
const parent = createRoute({
state: {
foo: Number,
},
})

const child = createRoute({
parent: parent,
})

expect(child.stateParams).toMatchObject({
foo: Number,
})
})

test('given parent, meta is combined', () => {
const parent = createRoute({
meta: {
foo: 123,
},
})

const child = createRoute({
parent: parent,
meta: {
bar: 'zoo',
},
})

expect(child.meta).toMatchObject({
foo: 123,
bar: 'zoo',
})
})

test('given parent and child without meta, meta matches parent', () => {
const parent = createRoute({
meta: {
foo: 123,
},
})

const child = createRoute({
parent: parent,
})

expect(child.meta).toMatchObject({
foo: 123,
})
})
15 changes: 9 additions & 6 deletions src/services/createRoute.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Component, markRaw } from 'vue'
import { CombineKey } from '@/services/combineKey'
import { CombineMeta } from '@/services/combineMeta'
import { CombinePath } from '@/services/combinePath'
import { CombineQuery } from '@/services/combineQuery'
import { CombineState } from '@/services/combineState'
Expand Down Expand Up @@ -31,7 +32,7 @@ export function createRoute<
const TQuery extends string | Query | undefined = undefined,
const TMeta extends RouteMeta = RouteMeta,
const TStateParams extends Record<string, Param> = Record<string, Param>
>(options: CreateRouteOptions<TName, TPath, TQuery, TMeta> & WithHooks & WithoutComponents & WithoutParent & (WithState<TStateParams> | WithoutState)): Route<ToKey<TName>, Host<'', {}>, ToPath<TPath>, ToQuery<TQuery>, TStateParams>
>(options: CreateRouteOptions<TName, TPath, TQuery, TMeta> & WithHooks & WithoutComponents & WithoutParent & (WithState<TStateParams> | WithoutState)): Route<ToKey<TName>, Host<'', {}>, ToPath<TPath>, ToQuery<TQuery>, TMeta, TStateParams>

export function createRoute<
const TParent extends Route,
Expand All @@ -40,7 +41,7 @@ export function createRoute<
const TQuery extends string | Query | undefined = undefined,
const TMeta extends RouteMeta = RouteMeta,
const TStateParams extends Record<string, Param> = Record<string, Param>
>(options: CreateRouteOptions<TName, TPath, TQuery, TMeta> & WithHooks & WithoutComponents & WithParent<TParent> & (WithState<TStateParams> | WithoutState)): Route<CombineKey<TParent['key'], ToKey<TName>>, Host<'', {}>, CombinePath<TParent['path'], ToPath<TPath>>, CombineQuery<TParent['query'], ToQuery<TQuery>>, CombineState<TStateParams, TParent['stateParams']>>
>(options: CreateRouteOptions<TName, TPath, TQuery, TMeta> & WithHooks & WithoutComponents & WithParent<TParent> & (WithState<TStateParams> | WithoutState)): Route<CombineKey<TParent['key'], ToKey<TName>>, Host<'', {}>, CombinePath<TParent['path'], ToPath<TPath>>, CombineQuery<TParent['query'], ToQuery<TQuery>>, CombineMeta<TMeta, TParent['meta']>, CombineState<TStateParams, TParent['stateParams']>>

export function createRoute<
TComponent extends Component,
Expand All @@ -49,7 +50,7 @@ export function createRoute<
const TQuery extends string | Query | undefined = undefined,
const TMeta extends RouteMeta = RouteMeta,
const TStateParams extends Record<string, Param> = Record<string, Param>
>(options: CreateRouteOptions<TName, TPath, TQuery, TMeta> & WithHooks & WithComponent<TComponent, RouteParams<TPath, TQuery>> & WithoutParent & (WithState<TStateParams> | WithoutState)): Route<ToKey<TName>, Host<'', {}>, ToPath<TPath>, ToQuery<TQuery>, TStateParams>
>(options: CreateRouteOptions<TName, TPath, TQuery, TMeta> & WithHooks & WithComponent<TComponent, RouteParams<TPath, TQuery>> & WithoutParent & (WithState<TStateParams> | WithoutState)): Route<ToKey<TName>, Host<'', {}>, ToPath<TPath>, ToQuery<TQuery>, TMeta, TStateParams>

export function createRoute<
TComponent extends Component,
Expand All @@ -59,7 +60,7 @@ export function createRoute<
const TQuery extends string | Query | undefined = undefined,
const TMeta extends RouteMeta = RouteMeta,
const TStateParams extends Record<string, Param> = Record<string, Param>
>(options: CreateRouteOptions<TName, TPath, TQuery, TMeta> & WithHooks & WithComponent<TComponent, RouteParams<TPath, TQuery, TParent>> & WithParent<TParent> & (WithState<TStateParams> | WithoutState)): Route<CombineKey<TParent['key'], ToKey<TName>>, Host<'', {}>, CombinePath<TParent['path'], ToPath<TPath>>, CombineQuery<TParent['query'], ToQuery<TQuery>>, CombineState<TStateParams, TParent['stateParams']>>
>(options: CreateRouteOptions<TName, TPath, TQuery, TMeta> & WithHooks & WithComponent<TComponent, RouteParams<TPath, TQuery, TParent>> & WithParent<TParent> & (WithState<TStateParams> | WithoutState)): Route<CombineKey<TParent['key'], ToKey<TName>>, Host<'', {}>, CombinePath<TParent['path'], ToPath<TPath>>, CombineQuery<TParent['query'], ToQuery<TQuery>>, CombineMeta<TMeta, TParent['meta']>, CombineState<TStateParams, TParent['stateParams']>>

export function createRoute<
TComponents extends Record<string, Component>,
Expand All @@ -68,7 +69,7 @@ export function createRoute<
const TQuery extends string | Query | undefined = undefined,
const TMeta extends RouteMeta = RouteMeta,
const TStateParams extends Record<string, Param> = Record<string, Param>
>(options: CreateRouteOptions<TName, TPath, TQuery, TMeta> & WithHooks & WithComponents<TComponents, RouteParams<TPath, TQuery>> & WithoutParent & (WithState<TStateParams> | WithoutState)): Route<ToKey<TName>, Host<'', {}>, ToPath<TPath>, ToQuery<TQuery>, TStateParams>
>(options: CreateRouteOptions<TName, TPath, TQuery, TMeta> & WithHooks & WithComponents<TComponents, RouteParams<TPath, TQuery>> & WithoutParent & (WithState<TStateParams> | WithoutState)): Route<ToKey<TName>, Host<'', {}>, ToPath<TPath>, ToQuery<TQuery>, TMeta, TStateParams>

export function createRoute<
TComponents extends Record<string, Component>,
Expand All @@ -78,12 +79,13 @@ export function createRoute<
const TQuery extends string | Query | undefined = undefined,
const TMeta extends RouteMeta = RouteMeta,
const TStateParams extends Record<string, Param> = Record<string, Param>
>(options: CreateRouteOptions<TName, TPath, TQuery, TMeta> & WithHooks & WithComponents<TComponents, RouteParams<TPath, TQuery, TParent>> & WithParent<TParent> & (WithState<TStateParams> | WithoutState)): Route<CombineKey<TParent['key'], ToKey<TName>>, Host<'', {}>, CombinePath<TParent['path'], ToPath<TPath>>, CombineQuery<TParent['query'], ToQuery<TQuery>>, CombineState<TStateParams, TParent['stateParams']>>
>(options: CreateRouteOptions<TName, TPath, TQuery, TMeta> & WithHooks & WithComponents<TComponents, RouteParams<TPath, TQuery, TParent>> & WithParent<TParent> & (WithState<TStateParams> | WithoutState)): Route<CombineKey<TParent['key'], ToKey<TName>>, Host<'', {}>, CombinePath<TParent['path'], ToPath<TPath>>, CombineQuery<TParent['query'], ToQuery<TQuery>>, CombineMeta<TMeta, TParent['meta']>, CombineState<TStateParams, TParent['stateParams']>>

export function createRoute(options: CreateRouteOptions): Route {
const key = toKey(options.name)
const path = toPath(options.path)
const query = toQuery(options.query)
const meta = options.meta ?? {}
const stateParams = isWithState(options) ? options.state : {}
const rawRoute = markRaw({ meta: {}, state: {}, ...options })

Expand All @@ -93,6 +95,7 @@ export function createRoute(options: CreateRouteOptions): Route {
key,
path,
query,
meta,
stateParams,
depth: 1,
host: host('', {}),
Expand Down
2 changes: 2 additions & 0 deletions src/types/createRouteOptions.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Component } from 'vue'
import { combineKey } from '@/services/combineKey'
import { combineMeta } from '@/services/combineMeta'
import { combinePath } from '@/services/combinePath'
import { combineQuery } from '@/services/combineQuery'
import { combineState } from '@/services/combineState'
Expand Down Expand Up @@ -135,6 +136,7 @@ export function combineRoutes(parent: Route, child: Route): Route {
key: combineKey(parent.key, child.key),
path: combinePath(parent.path, child.path),
query: combineQuery(parent.query, child.query),
meta: combineMeta(parent.meta, child.meta),
stateParams: combineState(parent.stateParams, child.stateParams),
matches: [...parent.matches, child.matched],
host: parent.host,
Expand Down
5 changes: 5 additions & 0 deletions src/types/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ export type Route<
THost extends Host = Host,
TPath extends Path = Path,
TQuery extends Query = Query,
TMeta extends RouteMeta = RouteMeta,
TStateParams extends Record<string, Param> = Record<string, Param>
> = {
/**
Expand Down Expand Up @@ -53,6 +54,10 @@ export type Route<
* Represents the structured query of the route, including query params.
*/
query: TQuery,
/**
* Represents additional metadata associated with a route, combined with any parents.
*/
meta: TMeta,
/**
* Represents the schema of the route state, combined with any parents.
*/
Expand Down
Loading