feat(core-realtime): channel-template matcher (placeholder extraction)
This commit is contained in:
33
packages/core-realtime/src/channel-template.test.ts
Normal file
33
packages/core-realtime/src/channel-template.test.ts
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
import { describe, it, expect } from "vitest";
|
||||||
|
import { matchChannelTemplate } from "@/channel-template";
|
||||||
|
|
||||||
|
describe("matchChannelTemplate", () => {
|
||||||
|
it("matches a plain channel name exactly", () => {
|
||||||
|
expect(matchChannelTemplate("blog.feed", "blog.feed")).toEqual({ params: {} });
|
||||||
|
expect(matchChannelTemplate("blog.feed", "blog.other")).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("matches a templated channel and extracts params", () => {
|
||||||
|
expect(
|
||||||
|
matchChannelTemplate("notifications.user.{userId}", "notifications.user.user_42"),
|
||||||
|
).toEqual({ params: { userId: "user_42" } });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns null when a templated channel doesn't match the shape", () => {
|
||||||
|
expect(
|
||||||
|
matchChannelTemplate("notifications.user.{userId}", "notifications.user"),
|
||||||
|
).toBeNull();
|
||||||
|
expect(
|
||||||
|
matchChannelTemplate("notifications.user.{userId}", "blog.feed"),
|
||||||
|
).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("supports multiple placeholders", () => {
|
||||||
|
expect(
|
||||||
|
matchChannelTemplate(
|
||||||
|
"rooms.{roomId}.user.{userId}",
|
||||||
|
"rooms.r1.user.u1",
|
||||||
|
),
|
||||||
|
).toEqual({ params: { roomId: "r1", userId: "u1" } });
|
||||||
|
});
|
||||||
|
});
|
||||||
28
packages/core-realtime/src/channel-template.ts
Normal file
28
packages/core-realtime/src/channel-template.ts
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
const PLACEHOLDER = /\{([a-zA-Z_][a-zA-Z0-9_]*)\}/g;
|
||||||
|
|
||||||
|
export function matchChannelTemplate(
|
||||||
|
template: string,
|
||||||
|
candidate: string,
|
||||||
|
): { params: Record<string, string> } | null {
|
||||||
|
// No placeholders: exact match.
|
||||||
|
if (!template.includes("{")) {
|
||||||
|
return template === candidate ? { params: {} } : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build a regex from the template, replacing {name} with named groups.
|
||||||
|
const names: string[] = [];
|
||||||
|
const escaped = template.replace(/[.+?^${}()|[\]\\]/g, "\\$&"); // escape regex specials
|
||||||
|
// The above escapes `{` and `}` too — restore them around placeholders.
|
||||||
|
const pattern = escaped.replace(/\\\{([a-zA-Z_][a-zA-Z0-9_]*)\\\}/g, (_m, name) => {
|
||||||
|
names.push(name);
|
||||||
|
return `([^.]+)`;
|
||||||
|
});
|
||||||
|
const re = new RegExp(`^${pattern}$`);
|
||||||
|
const match = candidate.match(re);
|
||||||
|
if (!match) return null;
|
||||||
|
const params: Record<string, string> = {};
|
||||||
|
names.forEach((name, i) => {
|
||||||
|
params[name] = match[i + 1]!;
|
||||||
|
});
|
||||||
|
return { params };
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user