Initial Commit
This commit is contained in:
@@ -0,0 +1,84 @@
|
||||
/**
|
||||
* Glob-style field-path matcher for masking rule evaluation.
|
||||
*
|
||||
* Supports three matching modes that cover the common cases for nested-object
|
||||
* traversal:
|
||||
* - Exact match (`"user.email"` → `"user.email"`)
|
||||
* - Single-segment wildcard (`*`) — matches any one dot-separated segment
|
||||
* - Multi-segment wildcard (`**`) — matches any number of dot-separated
|
||||
* segments (including zero when at the end of a pattern)
|
||||
*/
|
||||
|
||||
/**
|
||||
* Test whether a concrete `fieldPath` matches the supplied glob `pattern`.
|
||||
*
|
||||
* Rules:
|
||||
* - `*` matches exactly one path segment (characters between dots).
|
||||
* - `**` matches zero or more path segments.
|
||||
* - All other characters must match literally (case-sensitive).
|
||||
* - Leading/trailing dots are treated as empty segments and behave
|
||||
* consistently with the wildcard rules above.
|
||||
*
|
||||
* @param fieldPath - The concrete dot-notation path, e.g. `"user.address.city"`.
|
||||
* @param pattern - The glob pattern to test against, e.g. `"user.*"` or `"**.email"`.
|
||||
* @returns `true` when `fieldPath` satisfies `pattern`, `false` otherwise.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* matchPath('user.email', 'user.email'); // true — exact
|
||||
* matchPath('user.email', 'user.*'); // true — single-segment wildcard
|
||||
* matchPath('user.address.city', 'user.**'); // true — multi-segment wildcard
|
||||
* matchPath('billing.email', '**.email'); // true — leading multi-segment
|
||||
* matchPath('user.name', 'user.email'); // false — no match
|
||||
* ```
|
||||
*/
|
||||
export function matchPath(fieldPath: string, pattern: string): boolean {
|
||||
const pathSegs = fieldPath.split(".");
|
||||
const patSegs = pattern.split(".");
|
||||
return matchSegments(pathSegs, 0, patSegs, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursive backtracking matcher that aligns path segments with pattern
|
||||
* segments, handling `**` greedily with fall-back.
|
||||
*/
|
||||
function matchSegments(
|
||||
path: readonly string[],
|
||||
pi: number,
|
||||
pat: readonly string[],
|
||||
si: number
|
||||
): boolean {
|
||||
// Both exhausted → match.
|
||||
if (pi === path.length && si === pat.length) return true;
|
||||
|
||||
// Pattern exhausted but path has remaining segments → no match.
|
||||
if (si === pat.length) return false;
|
||||
|
||||
const seg = pat[si];
|
||||
|
||||
if (seg === "**") {
|
||||
// `**` can match zero or more path segments.
|
||||
// Try consuming zero segments (advance only pattern).
|
||||
if (matchSegments(path, pi, pat, si + 1)) return true;
|
||||
// Try consuming one path segment at a time.
|
||||
for (let consumed = pi; consumed < path.length; consumed++) {
|
||||
if (matchSegments(path, consumed + 1, pat, si + 1)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Path is exhausted but pattern still has non-`**` segments → no match.
|
||||
if (pi === path.length) return false;
|
||||
|
||||
// `*` matches any single segment.
|
||||
if (seg === "*") {
|
||||
return matchSegments(path, pi + 1, pat, si + 1);
|
||||
}
|
||||
|
||||
// Literal segment — must match exactly.
|
||||
if (path[pi] === seg) {
|
||||
return matchSegments(path, pi + 1, pat, si + 1);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
Reference in New Issue
Block a user