diff --git a/aio/tools/firebase-test-utils/FirebaseRedirect.spec.ts b/aio/tools/firebase-test-utils/FirebaseRedirect.spec.ts index db652cc1aa..257edbeb43 100644 --- a/aio/tools/firebase-test-utils/FirebaseRedirect.spec.ts +++ b/aio/tools/firebase-test-utils/FirebaseRedirect.spec.ts @@ -3,27 +3,27 @@ import { FirebaseRedirect } from './FirebaseRedirect'; describe('FirebaseRedirect', () => { describe('replace', () => { it('should return undefined if the redirect does not match the url', () => { - const redirect = new FirebaseRedirect('/a/b/c', '/x/y/z'); + const redirect = new FirebaseRedirect({source: '/a/b/c', destination: '/x/y/z'}); expect(redirect.replace('/1/2/3')).toBe(undefined); }); it('should return the destination if there is a match', () => { - const redirect = new FirebaseRedirect('/a/b/c', '/x/y/z'); + const redirect = new FirebaseRedirect({source: '/a/b/c', destination: '/x/y/z'}); expect(redirect.replace('/a/b/c')).toBe('/x/y/z'); }); it('should inject name params into the destination', () => { - const redirect = new FirebaseRedirect('/api/:package/:api-*', '<:package><:api>'); + const redirect = new FirebaseRedirect({source: '/api/:package/:api-*', destination: '<:package><:api>'}); expect(redirect.replace('/api/common/NgClass-directive')).toEqual(''); }); it('should inject rest params into the destination', () => { - const redirect = new FirebaseRedirect('/a/:rest*', '/x/:rest*/y'); + const redirect = new FirebaseRedirect({source: '/a/:rest*', destination: '/x/:rest*/y'}); expect(redirect.replace('/a/b/c')).toEqual('/x/b/c/y'); }); it('should inject both named and rest parameters into the destination', () => { - const redirect = new FirebaseRedirect('/:a/:rest*', '/x/:a/y/:rest*/z'); + const redirect = new FirebaseRedirect({source: '/:a/:rest*', destination: '/x/:a/y/:rest*/z'}); expect(redirect.replace('/a/b/c')).toEqual('/x/a/y/b/c/z'); }); }); diff --git a/aio/tools/firebase-test-utils/FirebaseRedirect.ts b/aio/tools/firebase-test-utils/FirebaseRedirect.ts index d0957e158c..212ae7f327 100644 --- a/aio/tools/firebase-test-utils/FirebaseRedirect.ts +++ b/aio/tools/firebase-test-utils/FirebaseRedirect.ts @@ -1,19 +1,25 @@ import * as XRegExp from 'xregexp'; -import { FirebaseGlob } from './FirebaseGlob'; +import { FirebaseRedirectConfig } from './FirebaseRedirector'; +import { FirebaseRedirectSource } from './FirebaseRedirectSource'; export class FirebaseRedirect { - glob = new FirebaseGlob(this.source); - constructor(public source: string, public destination: string) {} + source: FirebaseRedirectSource; + destination: string; + + constructor({source, destination}: FirebaseRedirectConfig) { + this.source = FirebaseRedirectSource.fromGlobPattern(source); + this.destination = destination; + } replace(url: string): string | undefined { - const match = this.glob.match(url); + const match = this.source.match(url); if (!match) { return undefined; } - const paramReplacers = Object.keys(this.glob.namedParams).map<[RegExp, string]>(name => [ XRegExp(`:${name}`, 'g'), match[name] ]); - const restReplacers = Object.keys(this.glob.restParams).map<[RegExp, string]>(name => [ XRegExp(`:${name}\\*`, 'g'), match[name] ]); - return XRegExp.replaceEach(this.destination, [...paramReplacers, ...restReplacers]); + const namedReplacers = this.source.namedGroups.map<[RegExp, string]>(name => [ XRegExp(`:${name}`, 'g'), match[name] ]); + const restReplacers = this.source.restNamedGroups.map<[RegExp, string]>(name => [ XRegExp(`:${name}\\*`, 'g'), match[name] ]); + return XRegExp.replaceEach(this.destination, [...namedReplacers, ...restReplacers]); } } diff --git a/aio/tools/firebase-test-utils/FirebaseGlob.spec.ts b/aio/tools/firebase-test-utils/FirebaseRedirectSource.spec.ts similarity index 80% rename from aio/tools/firebase-test-utils/FirebaseGlob.spec.ts rename to aio/tools/firebase-test-utils/FirebaseRedirectSource.spec.ts index 447ec98c92..4c2b95c2c2 100644 --- a/aio/tools/firebase-test-utils/FirebaseGlob.spec.ts +++ b/aio/tools/firebase-test-utils/FirebaseRedirectSource.spec.ts @@ -1,6 +1,6 @@ -import { FirebaseGlob } from './FirebaseGlob'; -describe('FirebaseGlob', () => { +import { FirebaseRedirectSource } from './FirebaseRedirectSource'; +describe('FirebaseRedirectSource', () => { describe('test', () => { it('should match * parts', () => { testGlob('asdf/*.jpg', @@ -42,12 +42,12 @@ describe('FirebaseGlob', () => { }); it('should error on non-supported choice groups', () => { - expect(() => new FirebaseGlob('/!(a|b)/c')) - .toThrowError('Error in FirebaseGlob: "/!(a|b)/c" - "not" expansions are not supported: "!(a|b)"'); - expect(() => new FirebaseGlob('/(a|b)/c')) - .toThrowError('Error in FirebaseGlob: "/(a|b)/c" - unknown expansion type: "/" in "/(a|b)"'); - expect(() => new FirebaseGlob('/&(a|b)/c')) - .toThrowError('Error in FirebaseGlob: "/&(a|b)/c" - unknown expansion type: "&" in "&(a|b)"'); + expect(() => FirebaseRedirectSource.fromGlobPattern('/!(a|b)/c')) + .toThrowError('Error in FirebaseRedirectSource: "/!(a|b)/c" - "not" expansions are not supported: "!(a|b)"'); + expect(() => FirebaseRedirectSource.fromGlobPattern('/(a|b)/c')) + .toThrowError('Error in FirebaseRedirectSource: "/(a|b)/c" - unknown expansion type: "/" in "/(a|b)"'); + expect(() => FirebaseRedirectSource.fromGlobPattern('/&(a|b)/c')) + .toThrowError('Error in FirebaseRedirectSource: "/&(a|b)/c" - unknown expansion type: "&" in "&(a|b)"'); }); // Globs that contain params tested via the match tests below @@ -176,14 +176,14 @@ describe('FirebaseGlob', () => { }); function testGlob(pattern: string, matches: string[], nonMatches: string[]) { - const glob = new FirebaseGlob(pattern); - matches.forEach(url => expect(glob.test(url)).toBe(true, url)); - nonMatches.forEach(url => expect(glob.test(url)).toBe(false, url)); + const redirectSource = FirebaseRedirectSource.fromGlobPattern(pattern); + matches.forEach(url => expect(redirectSource.test(url)).toBe(true, url)); + nonMatches.forEach(url => expect(redirectSource.test(url)).toBe(false, url)); } function testMatch(pattern: string, captures: { named?: string[], rest?: string[] }, matches: { [url: string]: object|undefined }) { - const glob = new FirebaseGlob(pattern); - expect(Object.keys(glob.namedParams)).toEqual(captures.named || []); - expect(Object.keys(glob.restParams)).toEqual(captures.rest || []); - Object.keys(matches).forEach(url => expect(glob.match(url)).toEqual(matches[url])); + const redirectSource = FirebaseRedirectSource.fromGlobPattern(pattern); + expect(redirectSource.namedGroups).toEqual(captures.named || []); + expect(redirectSource.restNamedGroups).toEqual(captures.rest || []); + Object.keys(matches).forEach(url => expect(redirectSource.match(url)).toEqual(matches[url])); } diff --git a/aio/tools/firebase-test-utils/FirebaseGlob.ts b/aio/tools/firebase-test-utils/FirebaseRedirectSource.ts similarity index 55% rename from aio/tools/firebase-test-utils/FirebaseGlob.ts rename to aio/tools/firebase-test-utils/FirebaseRedirectSource.ts index fa857e0409..51294427a4 100644 --- a/aio/tools/firebase-test-utils/FirebaseGlob.ts +++ b/aio/tools/firebase-test-utils/FirebaseRedirectSource.ts @@ -5,45 +5,51 @@ interface XRegExp extends RegExp { xregexp: { captureNames?: string[] }; } -const dot = /\./g; -const star = /\*/g; -const doubleStar = /(^|\/)\*\*($|\/)/g; // e.g. a/**/b or **/b or a/** but not a**b -const modifiedPatterns = /(.)\(([^)]+)\)/g; // e.g. `@(a|b) -const restParam = /\/:([A-Za-z]+)\*/g; // e.g. `:rest*` -const namedParam = /\/:([A-Za-z]+)/g; // e.g. `:api` -const possiblyEmptyInitialSegments = /^\.🐷\//g; // e.g. `**/a` can also match `a` -const possiblyEmptySegments = /\/\.🐷\//g; // e.g. `a/**/b` can also match `a/b` -const willBeStar = /🐷/g; // e.g. `a**b` not matched by previous rule +export class FirebaseRedirectSource { + regex = XRegExp(this.pattern) as XRegExp; + namedGroups: string[] = []; + + private constructor(public pattern: string, public restNamedGroups: string[] = []) { + const restNamedGroupsSet = new Set(restNamedGroups); + pattern.replace(/\(\?<([^>]+)>/g, (_, name) => { + if (!restNamedGroupsSet.has(name)) { + this.namedGroups.push(name); + } + return ''; + }); + } + + static fromGlobPattern(glob: string): FirebaseRedirectSource { + const dot = /\./g; + const star = /\*/g; + const doubleStar = /(^|\/)\*\*($|\/)/g; // e.g. a/**/b or **/b or a/** but not a**b + const modifiedPatterns = /(.)\(([^)]+)\)/g; // e.g. `@(a|b) + const restParam = /\/:([A-Za-z]+)\*/g; // e.g. `:rest*` + const namedParam = /\/:([A-Za-z]+)/g; // e.g. `:api` + const possiblyEmptyInitialSegments = /^\.🐷\//g; // e.g. `**/a` can also match `a` + const possiblyEmptySegments = /\/\.🐷\//g; // e.g. `a/**/b` can also match `a/b` + const willBeStar = /🐷/g; // e.g. `a**b` not matched by previous rule -export class FirebaseGlob { - pattern: string; - regex: XRegExp; - namedParams: { [key: string]: boolean } = {}; - restParams: { [key: string]: boolean } = {}; - constructor(glob: string) { try { + const restNamedGroups: string[] = []; const pattern = glob .replace(dot, '\\.') .replace(modifiedPatterns, replaceModifiedPattern) .replace(restParam, (_, param) => { // capture the rest of the string - this.restParams[param] = true; + restNamedGroups.push(param); return `(?:/(?<${param}>.🐷))?`; }) - .replace(namedParam, (_, param) => { - // capture the named parameter - this.namedParams[param] = true; - return `/(?<${param}>[^/]+)`; - }) + .replace(namedParam, `/(?<$1>[^/]+)`) .replace(doubleStar, '$1.🐷$2') // use the pig to avoid replacing ** in next rule .replace(star, '[^/]*') // match a single segment .replace(possiblyEmptyInitialSegments, '(?:.*)')// deal with **/ special cases .replace(possiblyEmptySegments, '(?:/|/.*/)') // deal with /**/ special cases .replace(willBeStar, '*'); // other ** matches - this.pattern = `^${pattern}$`; - this.regex = XRegExp(this.pattern) as XRegExp; - } catch (e) { - throw new Error(`Error in FirebaseGlob: "${glob}" - ${e.message}`); + + return new FirebaseRedirectSource(`^${pattern}$`, restNamedGroups); + } catch (err) { + throw new Error(`Error in FirebaseRedirectSource: "${glob}" - ${err.message}`); } } @@ -52,7 +58,7 @@ export class FirebaseGlob { } match(url: string): { [key: string]: string } | undefined { - const match = XRegExp.exec(url, this.regex) as ReturnType & { [captured: string]: string }; + const match = XRegExp.exec(url, this.regex) as ReturnType; if (!match) { return undefined; diff --git a/aio/tools/firebase-test-utils/FirebaseRedirector.ts b/aio/tools/firebase-test-utils/FirebaseRedirector.ts index 36af0420b7..19f6a73ebc 100644 --- a/aio/tools/firebase-test-utils/FirebaseRedirector.ts +++ b/aio/tools/firebase-test-utils/FirebaseRedirector.ts @@ -8,7 +8,7 @@ export interface FirebaseRedirectConfig { export class FirebaseRedirector { private redirects: FirebaseRedirect[]; constructor(redirects: FirebaseRedirectConfig[]) { - this.redirects = redirects.map(redirect => new FirebaseRedirect(redirect.source, redirect.destination)); + this.redirects = redirects.map(redirect => new FirebaseRedirect(redirect)); } redirect(url: string): string {