diff --git a/routers/MaskRouter.ts b/routers/MaskRouter.ts index b373d0e..3765d47 100644 --- a/routers/MaskRouter.ts +++ b/routers/MaskRouter.ts @@ -8,7 +8,7 @@ import { Router } from "./Router.ts"; import { type ServeResponseType } from "../types/ServeResponseType.ts"; import { type RouterOptions, createRequiredOptions } from "../helpers/RouterOptions.ts"; import { type ParamDeclarationsType } from "../types/ParamDeclarationsType.ts"; -import { type ParamValuesType } from "../types/ParamValuesType.ts"; +import { type ParsedParamValues } from "../types/ParsedParamValues.ts"; import { RouterMalformedException } from "./RouterMalformedException.ts"; @@ -38,37 +38,42 @@ export class MaskRouter extends Router implements IRouter { readonly #varibleOpenChar = '['; readonly #varibleCloseChar = ']'; - readonly #maskParser = /\<(?[a-z][A-z0-9]*)(=(?.+?))?\s*(\s+(?.+?))?\>/g; + readonly #maskParser = /\<(?[a-z][A-z0-9-]*)(=(?.+?))?\s*(\s+(?.+?))?\>/g; readonly #maskCache = new Cache(); readonly #variantCache = new Cache(); readonly #matchCache = new Cache(); readonly #paramParserCache = new Cache(); readonly #paramDeclarationCache = new Cache(); - readonly #paramValuesCache = new Cache(); + readonly #paramValuesCache = new Cache(); constructor(mask: string, serveResponse: ServeResponseType, options?: RouterOptions) { super(); - this.#mask = this.#parseMask(mask); + this.#mask = this.#normalizeMask(mask); this.#maskVariants = this.#parseVariants(mask); this.#serveResponse = serveResponse; this.#options = createRequiredOptions(options); } + /** + * Return `true` if `pathname` of request matches `mask`. + */ match(req: Request): boolean { const pathname = this.#computePathname(req); - return this.#match(pathname) } + /** + * Return Response. + */ async serveResponse(req: Request): Promise { const pathname = this.#computePathname(req); - const matchedMask = this.#getMatchedMask(pathname); + const matchedMask = this.#getMatchedMaskVariant(pathname); if (matchedMask === null || !this.#match(pathname)) throw new Error("No mask matched"); const params: Record = {}; @@ -88,6 +93,9 @@ export class MaskRouter extends Router implements IRouter { } + /** + * Return `true` if `pathname` matches some of `mask` variants. + */ #match(pathname: string): boolean { const result = this.#maskVariants.some(mask => { return this.#matchMask(mask, pathname); @@ -97,7 +105,10 @@ export class MaskRouter extends Router implements IRouter { } - #getMatchedMask(pathname: string): string | null { + /** + * Return matched mask variant or `null` if no mask matched. + */ + #getMatchedMaskVariant(pathname: string): string | null { const result = this.#maskVariants.find(mask => { return this.#matchMask(mask, pathname); }) ?? null; @@ -174,7 +185,7 @@ export class MaskRouter extends Router implements IRouter { } - #parseParamValues(primaryMask: string, matchedMask: string, pathname: string): ParamValuesType | null { + #parseParamValues(primaryMask: string, matchedMask: string, pathname: string): ParsedParamValues | null { const isValid = (value: string | null, expression: RegExp | null): boolean => { if (expression === null) return true; if (value === null) return false; @@ -185,7 +196,7 @@ export class MaskRouter extends Router implements IRouter { const parse = (primaryMask: string, matchedMask: string, pathname: string) => { const paramParser = this.#createParamParser(matchedMask); - const paramValues: ParamValuesType = new Map(); + const paramValues: ParsedParamValues = new Map(); const paramDeclarations = this.#parseParamDeclarations(primaryMask); if (paramDeclarations === null) return paramValues; @@ -324,7 +335,7 @@ export class MaskRouter extends Router implements IRouter { } - #parseMask(mask: string): string { + #normalizeMask(mask: string): string { const parse = (mask: string): string => { const openChar = this.#varibleOpenChar; const closeChar = this.#varibleCloseChar; @@ -344,4 +355,39 @@ export class MaskRouter extends Router implements IRouter { return Router.cleanPathname(pathname); } + + + recontructPathname(params: Record): string { + const paramNames = Object.keys(params); + + // Find matching mask + const mask = this.#maskVariants.find(mask => { + const declaration = this.#parseParamDeclarations(mask); + if (declaration === null) return false; + + for (const [name, properties] of declaration.entries()) { + // Missing param + if (!paramNames.includes(name) && properties.defaultValue === null) return false; + } + + return true; + }); + + // If mask not found, creates url query + if (!mask) { + const query = new URLSearchParams(params); + return query.toString(); + } + + const declaration = this.#parseParamDeclarations(mask); + + this.#maskParser.lastIndex = 0; + const pathname = mask.replace(this.#maskParser, (_substring, _g1, _g2, _g3, _g4, _g5, _offset, _source, groups) => { + const name = groups.name; + + return params[name] ?? declaration?.get(name)?.defaultValue ?? ''; + }); + + return pathname; + } } diff --git a/routers/RouterList.ts b/routers/RouterList.ts index 6ea20a9..054b43b 100644 --- a/routers/RouterList.ts +++ b/routers/RouterList.ts @@ -50,14 +50,6 @@ export class RouterList implements IRouter { } - /** - * @deprecated Use `add` method instead. - */ - addRouter(router: IRouter): void { - this.add(router); - } - - startsWith(path: string): RouterList { const searchString = Router.cleanPathname(path); @@ -102,6 +94,15 @@ export class RouterList implements IRouter { } + // #region — Routers + /** + * @deprecated Use `add` method instead. + */ + addRouter(router: IRouter): void { + this.add(router); + } + + /** * Add route or router to list. * @@ -157,6 +158,7 @@ export class RouterList implements IRouter { const router = new RegExpRouter(regexp, serveResponse, this.#options); return this.#addRouter(router); } + // #endregion async match(req: Request): Promise { @@ -190,6 +192,7 @@ export class RouterList implements IRouter { } + // #region — Errors setError(serveResponse: ServeResponseType): void { this.#errorServeResponse = serveResponse; } @@ -213,4 +216,5 @@ export class RouterList implements IRouter { }); } } + // #endregion } diff --git a/tests/MaskRouter.test.ts b/tests/MaskRouter.test.ts index 465745e..61f92f9 100644 --- a/tests/MaskRouter.test.ts +++ b/tests/MaskRouter.test.ts @@ -163,3 +163,40 @@ Deno.test("MaskRouter::parseParams", async () => { await test(task); } }); + + +Deno.test("MaskRouter::recontructPathname", () => { + type Task = { + mask: string, + params: Record, + expectation: string, + } + + const tasks: Task[] = [ + { + expectation: 'product/detail/abc123', + mask: '//', + params: { + controller: "product", + action: "detail", + id: "abc123", + } + }, + { + expectation: 'mrkev', + mask: '', + params: { + "vegetable-name": "mrkev", + } + }, + ]; + + + tasks.forEach(({ mask, params, expectation }) => { + const serveResponse = () => new Response(); + const router = new MaskRouter(mask, serveResponse); + const urlPath = router.recontructPathname(params) + + assertEquals(urlPath, expectation); + }); +}); diff --git a/types/ParamValuesType.ts b/types/ParsedParamValues.ts similarity index 78% rename from types/ParamValuesType.ts rename to types/ParsedParamValues.ts index 696f663..1230941 100644 --- a/types/ParamValuesType.ts +++ b/types/ParsedParamValues.ts @@ -6,7 +6,7 @@ /** * @internal */ -export type ParamValuesType = Map