/** * 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; }