From 94b256781533302067c97b8de8fd4e607e3369d4 Mon Sep 17 00:00:00 2001 From: ByungJoon Lee Date: Tue, 6 Feb 2024 00:48:00 +0900 Subject: [PATCH] feat(sorter): apply sorter on route configurations --- .../avengers/heroes/[id]/{post.ts => get.ts} | 0 examples/handlers/avengers/heroes/[id]/put.ts | 2 +- examples/handlers/get.ts | 3 + src/modules/commands/routing.ts | 3 +- .../sort/__tests__/sort.route.path.test.ts | 162 ++++++++++++++++++ src/routes/sort/sortRoutePath.ts | 64 +++++++ 6 files changed, 232 insertions(+), 2 deletions(-) rename examples/handlers/avengers/heroes/[id]/{post.ts => get.ts} (100%) create mode 100644 examples/handlers/get.ts create mode 100644 src/routes/sort/__tests__/sort.route.path.test.ts create mode 100644 src/routes/sort/sortRoutePath.ts diff --git a/examples/handlers/avengers/heroes/[id]/post.ts b/examples/handlers/avengers/heroes/[id]/get.ts similarity index 100% rename from examples/handlers/avengers/heroes/[id]/post.ts rename to examples/handlers/avengers/heroes/[id]/get.ts diff --git a/examples/handlers/avengers/heroes/[id]/put.ts b/examples/handlers/avengers/heroes/[id]/put.ts index 54b3b3a..68903c9 100644 --- a/examples/handlers/avengers/heroes/[id]/put.ts +++ b/examples/handlers/avengers/heroes/[id]/put.ts @@ -2,6 +2,6 @@ import type { HTTPMethods } from 'fastify'; export const methods: HTTPMethods[] = ['PATCH']; -export async function hello() { +export async function handler() { return 'hello'; } diff --git a/examples/handlers/get.ts b/examples/handlers/get.ts new file mode 100644 index 0000000..f18d7f6 --- /dev/null +++ b/examples/handlers/get.ts @@ -0,0 +1,3 @@ +export async function handler() { + return 'hello'; +} diff --git a/src/modules/commands/routing.ts b/src/modules/commands/routing.ts index 45903a0..c73b4b8 100644 --- a/src/modules/commands/routing.ts +++ b/src/modules/commands/routing.ts @@ -15,6 +15,7 @@ import { getIncludePatterns } from '#/modules/files/getIncludePatterns'; import { ExcludeContainer } from '#/modules/scopes/ExcludeContainer'; import { IncludeContainer } from '#/modules/scopes/IncludeContainer'; import { defaultExclude } from '#/modules/scopes/defaultExclude'; +import { sortRoutePath } from '#/routes/sort/sortRoutePath'; import chalk from 'chalk'; import consola from 'consola'; import { isDescendant } from 'my-node-fp'; @@ -93,7 +94,7 @@ export async function routing(optionParams: TRouteOption, projectParams?: tsm.Pr const result = { imports: appendFastifyInstance(dedupeImportConfiguration(importConfigurations)), - routes: routeConfigurations, + routes: sortRoutePath(routeConfigurations), reasons, }; diff --git a/src/routes/sort/__tests__/sort.route.path.test.ts b/src/routes/sort/__tests__/sort.route.path.test.ts new file mode 100644 index 0000000..15a4bdd --- /dev/null +++ b/src/routes/sort/__tests__/sort.route.path.test.ts @@ -0,0 +1,162 @@ +import type { IRouteConfiguration } from '#/routes/interfaces/IRouteConfiguration'; +import { sortRoutePath } from '#/routes/sort/sortRoutePath'; +import { describe, expect, it } from 'vitest'; + +const routings: IRouteConfiguration[] = [ + { + methods: ['get'], + routePath: '/', + hash: 'jjaZTV7G4bZewxbekhbGVgAfZRoDcCBP', + hasOption: false, + handlerName: 'handler_jjaZTV7G4bZewxbekhbGVgAfZRoDcCBP', + typeArgument: { + request: 'property-signature', + text: '', + }, + sourceFilePath: '/Users/imjuni/project/github/fast-maker/examples/handlers/get.ts', + }, + { + methods: ['get'], + routePath: '/avengers/heroes/:id', + hash: 'E6sTFTY5HVG6ZoiyOfO78twMQtegfNKM', + hasOption: false, + handlerName: 'handler_E6sTFTY5HVG6ZoiyOfO78twMQtegfNKM', + typeArgument: { + request: 'fastify-request', + kind: 187, + text: '{ Body: IAbility }', + }, + sourceFilePath: '/Users/imjuni/project/github/fast-maker/examples/handlers/avengers/heroes/[id]/get.ts', + }, + { + methods: ['put', 'patch'], + routePath: '/avengers/heroes/:id', + hash: 'JcsG7kAN9Rhl7Bj66sC4AJjcG81hc0o7', + hasOption: false, + handlerName: 'handler_JcsG7kAN9Rhl7Bj66sC4AJjcG81hc0o7', + typeArgument: { + request: 'property-signature', + text: '', + }, + sourceFilePath: '/Users/imjuni/project/github/fast-maker/examples/handlers/avengers/heroes/[id]/put.ts', + }, + { + methods: ['get', 'search'], + routePath: '/avengers/heroes', + hash: 'RKQsowJ59rvtTzit3ceAu0YOnIOLK4ZH', + hasOption: false, + handlerName: 'handler_RKQsowJ59rvtTzit3ceAu0YOnIOLK4ZH', + typeArgument: { + request: 'property-signature', + text: '', + }, + sourceFilePath: '/Users/imjuni/project/github/fast-maker/examples/handlers/avengers/heroes/get.ts', + }, + { + methods: ['post'], + routePath: '/avengers/heroes', + hash: 'PyYwrekj8FU77SIYAcghL29WTKosRRP0', + hasOption: false, + handlerName: 'handler_PyYwrekj8FU77SIYAcghL29WTKosRRP0', + typeArgument: { + request: 'property-signature', + text: '', + }, + sourceFilePath: '/Users/imjuni/project/github/fast-maker/examples/handlers/avengers/heroes/post.ts', + }, + { + methods: ['get', 'search'], + routePath: '/avengers/heroes/:id?', + hash: 'dIptpGuAKFzf0qeTAHtzx2eULDtPLFdB', + hasOption: false, + handlerName: 'handler_dIptpGuAKFzf0qeTAHtzx2eULDtPLFdB', + typeArgument: { + request: 'property-signature', + text: '', + }, + sourceFilePath: '/Users/imjuni/project/github/fast-maker/examples/handlers/avengers/heroes/[[id]]/get.ts', + }, + { + methods: ['post'], + routePath: '/avengers/heroes/:id/:hour(^\\d{2})h:minute(^\\d{2})m', + hash: 'r4PZQn5zGL7qwViJ1L07z1WK9sbfNLFK', + hasOption: false, + handlerName: 'handler_r4PZQn5zGL7qwViJ1L07z1WK9sbfNLFK', + typeArgument: { + request: 'fastify-request', + kind: 187, + text: '{ Body: IAbility }', + }, + sourceFilePath: '/Users/imjuni/project/github/fast-maker/examples/handlers/avengers/heroes/[id]/[$time]/post.ts', + }, + { + methods: ['delete'], + routePath: '/hello', + hash: 'DggTo0LTLK8Hpxjtd4DZ2DL2WkuKa9pL', + hasOption: false, + handlerName: 'handler_DggTo0LTLK8Hpxjtd4DZ2DL2WkuKa9pL', + typeArgument: { + request: 'property-signature', + text: '', + }, + sourceFilePath: '/Users/imjuni/project/github/fast-maker/examples/handlers/hello/delete.ts', + }, + { + methods: ['post'], + routePath: '/hello', + hash: 'yMyTHNwiACuaCaoPmXsAMsWdfIhWMJCS', + hasOption: false, + handlerName: 'handler_yMyTHNwiACuaCaoPmXsAMsWdfIhWMJCS', + typeArgument: { + request: 'property-signature', + text: '', + }, + sourceFilePath: '/Users/imjuni/project/github/fast-maker/examples/handlers/hello/post.ts', + }, + { + methods: ['put'], + routePath: '/hello', + hash: 'qFaWzYBLA8Du9hWEGQsjrEPqLbauwjQN', + hasOption: false, + handlerName: 'handler_qFaWzYBLA8Du9hWEGQsjrEPqLbauwjQN', + typeArgument: { + request: 'property-signature', + text: '', + }, + sourceFilePath: '/Users/imjuni/project/github/fast-maker/examples/handlers/hello/put.ts', + }, + { + methods: ['get'], + routePath: '/po-ke/world', + hash: 'm7vOoboyWbScOSEpWeCimw2qbMtNNrSc', + hasOption: true, + handlerName: 'handler_m7vOoboyWbScOSEpWeCimw2qbMtNNrSc', + typeArgument: { + request: 'fastify-request', + kind: 187, + text: "{\n Querystring: IReqPokeHello['querystring'];\n Body: IReqPokeHello['Body'];\n Headers: {\n 'access-token': string;\n 'refresh-token': string;\n 'expire-time': {\n token: string;\n expire: number;\n site: {\n host: string;\n port: number;\n };\n };\n };\n }", + }, + sourceFilePath: '/Users/imjuni/project/github/fast-maker/examples/handlers/po-ke/world/get.ts', + }, +]; + +describe('sortRoutePath', () => { + it('sorting', () => { + const sorted = sortRoutePath(routings); + const routePaths = sorted.map((route) => `${route.methods.join(', ')}-${route.routePath}`); + + expect(routePaths).toEqual([ + 'get-/', + 'get-/avengers/heroes/:id', + 'put, patch-/avengers/heroes/:id', + 'get, search-/avengers/heroes', + 'post-/avengers/heroes', + 'get, search-/avengers/heroes/:id?', + 'post-/avengers/heroes/:id/:hour(^\\d{2})h:minute(^\\d{2})m', + 'delete-/hello', + 'post-/hello', + 'put-/hello', + 'get-/po-ke/world', + ]); + }); +}); diff --git a/src/routes/sort/sortRoutePath.ts b/src/routes/sort/sortRoutePath.ts new file mode 100644 index 0000000..b589757 --- /dev/null +++ b/src/routes/sort/sortRoutePath.ts @@ -0,0 +1,64 @@ +import type { IRouteConfiguration } from '#/routes/interfaces/IRouteConfiguration'; +import consola from 'consola'; +import { isError, type PartialRecord } from 'my-easy-fp'; +import { isDescendant } from 'my-node-fp'; + +export function sortRoutePath(routes: IRouteConfiguration[]): IRouteConfiguration[] { + if (routes.length <= 0) { + return routes; + } + + const desc = routes.sort((l, r) => r.routePath.localeCompare(l.routePath)); + + const chunked = desc.reduce<{ prev: string; chunk: PartialRecord }>( + (chunking, current) => { + const next = { ...chunking }; + + try { + // skip chunking action for root route path + if (current.routePath === '/') { + next.chunk[current.routePath] = [current]; + return next; + } + + if (chunking.prev === '') { + next.prev = current.routePath; + next.chunk[current.routePath] = [...(next.chunk[current.routePath] ?? []), current]; + return next; + } + + if (isDescendant(current.routePath, chunking.prev, '/') === true) { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const backup = next.chunk[next.prev]!; + + next.chunk[next.prev] = undefined; + next.prev = current.routePath; + next.chunk[current.routePath] = [...backup, current]; + + return next; + } + + next.prev = current.routePath; + next.chunk[current.routePath] = [current]; + + return next; + } catch (caught) { + const err = isError(caught, new Error('unknown error raised')); + + consola.debug(err.message); + consola.debug(err.stack); + + return next; + } + }, + { prev: '', chunk: {} }, + ); + + const keys = Object.keys(chunked.chunk); + const asc = keys.sort((l, r) => l.localeCompare(r)); + + return asc + .map((key) => chunked.chunk[key]) + .filter((value): value is IRouteConfiguration[] => value != null) + .flat(); +}