Skip to content

Commit

Permalink
fix(app): add path matching
Browse files Browse the repository at this point in the history
  • Loading branch information
juanrgm committed Nov 15, 2024
1 parent 36cd460 commit 629d0bd
Show file tree
Hide file tree
Showing 5 changed files with 111 additions and 6 deletions.
5 changes: 5 additions & 0 deletions .changeset/fifty-lemons-wonder.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@dreamkit/app": patch
---

Fix path matching
6 changes: 4 additions & 2 deletions packages/app/src/RequestUrl.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import { kindApp } from "./utils/kind.js";
import { createRoutePathRegex, extractPathParams } from "./utils/routing.js";

export class RequestUrl<T = string> extends URL {
static {
kindApp(this, "RequestUrl");
}
is(...paths: (keyof T)[]): boolean {
// [review]
return paths.includes(this.pathname as any);
return paths.some((path) =>
createRoutePathRegex(path as string).test(this.pathname),
);
}
}
10 changes: 9 additions & 1 deletion packages/app/src/builders/RouteBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
ObjectType,
ObjectTypeProps,
s,
type Type,
} from "@dreamkit/schema";
import type { Merge } from "@dreamkit/utils/ts.js";

Expand Down Expand Up @@ -119,7 +120,14 @@ export class RouteBuilder<T extends RouteData = RouteData> {
if (typeof value === "string") return this.clone({ path: value }) as this;
const params: any = new Proxy(
{},
{ get: (_, prop) => `:${prop as string}` },
{
get: (_, prop: string) => {
const optional = (
this.options.params?.props?.[prop as string] as Type
).options.optional;
return `:${prop}${optional ? "?" : ""}`;
},
},
);
return this.clone({ path: value(params) });
}
Expand Down
33 changes: 30 additions & 3 deletions packages/app/src/utils/routing.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,48 @@
export function extractPathParams(
path: string,
): { spread: boolean; name: string; key: string }[] {
return [...path.matchAll(/([:\*])(\w+)/g)].map((match) => ({
): { spread: boolean; name: string; key: string; optional?: boolean }[] {
return [...path.matchAll(/([:\*])(\w+)(\?)?/g)].map((match) => ({
key: match[0],
spread: match[1] === "*",
name: match[2],
optional: match[3] === "?",
}));
}

export function createRoutePathRegex(path: string) {
const paramNames = extractPathParams(path);
let pattern = path.replace(/\//g, "\\/");
const optionalPattern = "([^\\/]+)?";
const endOptionalPattern = `\\/${optionalPattern}`;
const spreadPattern = "(.*)";
const endOptionalSpreadPattern = `\\/${spreadPattern}`;
for (const param of paramNames) {
if (param.spread) {
pattern = pattern.replace(param.key, spreadPattern);
} else if (param.optional) {
pattern = pattern.replace(param.key, optionalPattern);
} else {
pattern = pattern.replace(param.key, "([^\\/]+)");
}
}
if (pattern.endsWith(endOptionalSpreadPattern)) {
pattern = pattern.slice(0, -endOptionalSpreadPattern.length) + `(\\/.*)?`;
} else if (pattern.endsWith(endOptionalPattern)) {
pattern = pattern.slice(0, -endOptionalPattern.length) + `(\\/[^\\/]+)?`;
}
return new RegExp(`^${pattern}\/?$`);
}

export function createRouteUrl(
path: string,
params: Record<string, any> = {},
base = globalThis.location?.origin ?? "http://localhost",
) {
const paramNames = extractPathParams(path);
for (const param of paramNames)
path = path.replaceAll(param.key, params[param.name]);
path = path.replaceAll(param.key, params[param.name] ?? "");

if (path.length > 1 && path.endsWith("/")) path = path.slice(0, -1);

const url = new URL(path, base);

Expand Down
63 changes: 63 additions & 0 deletions packages/app/test/utils/routing.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import {
createRoutePathRegex,
createRouteUrl,
extractPathParams,
} from "../../src/utils/routing.js";
import { describe, it, expect } from "vitest";

describe("extractPathParams", () => {
it("extract optional param", () => {
expect(extractPathParams("/users/:id?")).toMatchObject([
{
key: ":id?",
name: "id",
optional: true,
spread: false,
},
]);
});
});

describe("createRouteUrl", () => {
it("with optional param", () => {
expect(createRouteUrl("/users/:id?", { id: 1 }).pathname).toBe("/users/1");
expect(createRouteUrl("/users/:id?").pathname).toBe("/users");
});
});

describe("createRoutePathRegex", () => {
it("with optional param", () => {
const regex = createRoutePathRegex("/users/:id?");
expect(regex.test("/users")).toBe(true);
expect(regex.test("/users/")).toBe(true);
expect(regex.test("/users/1")).toBe(true);
expect(regex.test("/users/1/")).toBe(true);
expect(regex.test("/users/1/2")).toBe(false);
});
it("with required param", () => {
const regex = createRoutePathRegex("/users/:id");
expect(regex.test("/users")).toBe(false);
expect(regex.test("/users/")).toBe(false);
expect(regex.test("/users/1")).toBe(true);
expect(regex.test("/users/1/")).toBe(true);
expect(regex.test("/users/1/2")).toBe(false);
});
it("with spread", () => {
const regex = createRoutePathRegex("/users/*any");
expect(regex.test("/")).toBe(false);
expect(regex.test("/users")).toBe(true);
expect(regex.test("/users/")).toBe(true);
expect(regex.test("/users/1")).toBe(true);
expect(regex.test("/users/1/")).toBe(true);
expect(regex.test("/users/1/2")).toBe(true);
});
it("with all", () => {
const regex = createRoutePathRegex("/*any");
expect(regex.test("/")).toBe(true);
expect(regex.test("/users")).toBe(true);
expect(regex.test("/users/")).toBe(true);
expect(regex.test("/users/1")).toBe(true);
expect(regex.test("/users/1/")).toBe(true);
expect(regex.test("/users/1/2")).toBe(true);
});
});

0 comments on commit 629d0bd

Please sign in to comment.