Skip to content

Commit

Permalink
Merge pull request #24 from adamjosefus/development
Browse files Browse the repository at this point in the history
Add `recontructPathname` method to `MaskRouter`
  • Loading branch information
adamjosefus authored Jun 28, 2022
2 parents 418365f + 9f4da3d commit dcbf457
Show file tree
Hide file tree
Showing 4 changed files with 106 additions and 19 deletions.
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

0 comments on commit dcbf457

Please sign in to comment.