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

Add recontructPathname method to MaskRouter #24

Merged
merged 9 commits into from
Jun 28, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
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
66 changes: 56 additions & 10 deletions routers/MaskRouter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";


Expand Down Expand Up @@ -38,37 +38,42 @@ export class MaskRouter extends Router implements IRouter {

readonly #varibleOpenChar = '[';
readonly #varibleCloseChar = ']';
readonly #maskParser = /\<(?<name>[a-z][A-z0-9]*)(=(?<defaultValue>.+?))?\s*(\s+(?<expression>.+?))?\>/g;
readonly #maskParser = /\<(?<name>[a-z][A-z0-9-]*)(=(?<defaultValue>.+?))?\s*(\s+(?<expression>.+?))?\>/g;

readonly #maskCache = new Cache<string>();
readonly #variantCache = new Cache<string[]>();
readonly #matchCache = new Cache<boolean>();
readonly #paramParserCache = new Cache<RegExp>();
readonly #paramDeclarationCache = new Cache<ParamDeclarationsType>();
readonly #paramValuesCache = new Cache<ParamValuesType | null>();
readonly #paramValuesCache = new Cache<ParsedParamValues | null>();


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<Response> {
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<string, string> = {};
Expand All @@ -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);
Expand All @@ -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;
Expand Down Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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;
Expand All @@ -344,4 +355,39 @@ export class MaskRouter extends Router implements IRouter {

return Router.cleanPathname(pathname);
}


recontructPathname(params: Record<string, string>): 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;
}
}
20 changes: 12 additions & 8 deletions routers/RouterList.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down Expand Up @@ -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.
*
Expand Down Expand Up @@ -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<boolean> {
Expand Down Expand Up @@ -190,6 +192,7 @@ export class RouterList implements IRouter {
}


// #region — Errors
setError(serveResponse: ServeResponseType): void {
this.#errorServeResponse = serveResponse;
}
Expand All @@ -213,4 +216,5 @@ export class RouterList implements IRouter {
});
}
}
// #endregion
}
37 changes: 37 additions & 0 deletions tests/MaskRouter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -163,3 +163,40 @@ Deno.test("MaskRouter::parseParams", async () => {
await test(task);
}
});


Deno.test("MaskRouter::recontructPathname", () => {
type Task = {
mask: string,
params: Record<string, string>,
expectation: string,
}

const tasks: Task[] = [
{
expectation: 'product/detail/abc123',
mask: '<controller>/<action>/<id>',
params: {
controller: "product",
action: "detail",
id: "abc123",
}
},
{
expectation: 'mrkev',
mask: '<vegetable-name>',
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);
});
});
2 changes: 1 addition & 1 deletion types/ParamValuesType.ts → types/ParsedParamValues.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
/**
* @internal
*/
export type ParamValuesType = Map<string, {
export type ParsedParamValues = Map<string, {
readonly order: number,
readonly value: string | null,
readonly valid: boolean,
Expand Down