Initial Commit
This commit is contained in:
@@ -0,0 +1,109 @@
|
||||
import {
|
||||
RATE_LIMIT_PLUGIN_CODES,
|
||||
RateLimitPluginError,
|
||||
} from "../error-codes/error-codes.js";
|
||||
|
||||
/**
|
||||
* A compiled path pattern ready for matching against inbound request
|
||||
* paths. Created once per route rule at plugin init and reused per
|
||||
* request.
|
||||
*/
|
||||
export interface CompiledPathPattern {
|
||||
readonly source: string;
|
||||
readonly regex: RegExp;
|
||||
readonly paramNames: readonly string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Result of matching a compiled pattern against a path.
|
||||
*/
|
||||
export interface PathMatchResult {
|
||||
readonly matched: boolean;
|
||||
readonly params: Readonly<Record<string, string>>;
|
||||
}
|
||||
|
||||
const PARAM_RE = /:[A-Za-z_][A-Za-z0-9_]*/g;
|
||||
|
||||
function escapeRegex(literal: string): string {
|
||||
return literal.replace(/[.+?^${}()|[\]\\*]/g, "\\$&");
|
||||
}
|
||||
|
||||
/**
|
||||
* Compile a Hono-style path pattern into a `CompiledPathPattern`.
|
||||
*
|
||||
* Supported forms:
|
||||
* - exact literal: `"/api/users"` matches only `/api/users`
|
||||
* - trailing wildcard: `"/api/users/*"` matches anything under
|
||||
* `/api/users/`
|
||||
* - inline wildcard: `"/api/*"` matches one path segment
|
||||
* - named param: `"/api/users/:id"` captures `id`
|
||||
*
|
||||
* Throws `RateLimitPluginError(INVALID_PATH_PATTERN)` when the
|
||||
* compiled regex cannot be built — defensive guard so any future
|
||||
* pattern-syntax mistake surfaces via the typed code rather than a
|
||||
* bare `SyntaxError`.
|
||||
*/
|
||||
export function compilePathPattern(source: string): CompiledPathPattern {
|
||||
const paramNames: string[] = [];
|
||||
let cursor = 0;
|
||||
let regexBody = "";
|
||||
PARAM_RE.lastIndex = 0;
|
||||
for (;;) {
|
||||
const match = PARAM_RE.exec(source);
|
||||
if (match === null) break;
|
||||
const literal = source.slice(cursor, match.index);
|
||||
regexBody += escapeRegex(literal).replace(/\\\*/g, "[^/]+");
|
||||
const name = match[0].slice(1);
|
||||
paramNames.push(name);
|
||||
regexBody += "([^/]+)";
|
||||
cursor = match.index + match[0].length;
|
||||
}
|
||||
const tail = source.slice(cursor);
|
||||
if (tail.endsWith("/*")) {
|
||||
regexBody += escapeRegex(tail.slice(0, -2)).replace(/\\\*/g, "[^/]+");
|
||||
regexBody += "(?:/.*)?";
|
||||
} else {
|
||||
regexBody += escapeRegex(tail).replace(/\\\*/g, "[^/]+");
|
||||
}
|
||||
let regex: RegExp;
|
||||
try {
|
||||
regex = new RegExp(`^${regexBody}$`);
|
||||
} catch (err) {
|
||||
throw new RateLimitPluginError(
|
||||
RATE_LIMIT_PLUGIN_CODES.INVALID_PATH_PATTERN,
|
||||
`Failed to compile path pattern '${source}'`,
|
||||
{ cause: err, context: { source, statusCode: 500 } }
|
||||
);
|
||||
}
|
||||
return { source, regex, paramNames };
|
||||
}
|
||||
|
||||
/**
|
||||
* Test a compiled pattern against a concrete path. Returns a
|
||||
* `PathMatchResult` carrying any captured params keyed by the names
|
||||
* declared in the source pattern.
|
||||
*/
|
||||
export function matchPath(
|
||||
pattern: CompiledPathPattern,
|
||||
path: string
|
||||
): PathMatchResult {
|
||||
const match = pattern.regex.exec(path);
|
||||
if (match === null) return { matched: false, params: Object.freeze({}) };
|
||||
const params: Record<string, string> = {};
|
||||
for (let i = 0; i < pattern.paramNames.length; i++) {
|
||||
const name = pattern.paramNames[i] as string;
|
||||
// `([^/]+)` is non-optional so a successful regex match guarantees
|
||||
// every capture group resolved to a non-empty string.
|
||||
params[name] = match[i + 1] as string;
|
||||
}
|
||||
return { matched: true, params: Object.freeze(params) };
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience helper: compile and match in a single call. Suitable for
|
||||
* one-off tests; in production the plugin caches `CompiledPathPattern`
|
||||
* per route rule.
|
||||
*/
|
||||
export function isPathMatch(source: string, path: string): boolean {
|
||||
return matchPath(compilePathPattern(source), path).matched;
|
||||
}
|
||||
Reference in New Issue
Block a user