diff --git a/packages/insomnia-smoke-test/tests/smoke/pre-request-script-ui.test.ts b/packages/insomnia-smoke-test/tests/smoke/pre-request-script-ui.test.ts index 2812683238..8a90590d26 100644 --- a/packages/insomnia-smoke-test/tests/smoke/pre-request-script-ui.test.ts +++ b/packages/insomnia-smoke-test/tests/smoke/pre-request-script-ui.test.ts @@ -72,10 +72,16 @@ test.describe('pre-request UI tests', async () => { { name: 'require / require classes from insomnia-collection module', preReqScript: ` - const { Property, Header, Variable } = require('insomnia-collection'); + const { Property, Header, Variable, QueryParam, Url } = require('insomnia-collection'); const prop = new Property('pid', 'pname'); const header = new Header({ key: 'headerKey', value: 'headerValue' }); const variable = new Variable({ key: 'headerKey', value: 'headerValue' }); + const qParam = new QueryParam({ key: 'queryKey', value: 'queryValue' }); + const url = new Url({ + host: ['insomnia', 'rest'], + path: ['path1', 'path2'], + protocol: 'https', + }); // set part of values insomnia.environment.set('propJson', JSON.stringify(prop.toJSON())); insomnia.environment.set('headerJson', JSON.stringify(header.toJSON())); diff --git a/packages/insomnia/src/sdk/objects/__tests__/headers.test.ts b/packages/insomnia/src/sdk/objects/__tests__/headers.test.ts index cb34d07463..a1abd9201f 100644 --- a/packages/insomnia/src/sdk/objects/__tests__/headers.test.ts +++ b/packages/insomnia/src/sdk/objects/__tests__/headers.test.ts @@ -1,9 +1,11 @@ import { describe, expect, it } from '@jest/globals'; import { Header } from '../headers'; +// import { QueryParam, setUrlParser, Url, UrlMatchPattern } from '../urls'; describe('test Header object', () => { it('test basic operations', () => { + // const header = new Header('Content-Type: application/json\nUser-Agent: MyClientLibrary/2.0\n'); const headerStr = 'Content-Type: application/json\nUser-Agent: MyClientLibrary/2.0\n'; const headerObjs = [ { key: 'Content-Type', value: 'application/json' }, diff --git a/packages/insomnia/src/sdk/objects/__tests__/urls.test.ts b/packages/insomnia/src/sdk/objects/__tests__/urls.test.ts new file mode 100644 index 0000000000..25d06fd58c --- /dev/null +++ b/packages/insomnia/src/sdk/objects/__tests__/urls.test.ts @@ -0,0 +1,171 @@ +import url from 'node:url'; + +import { describe, expect, it } from '@jest/globals'; + +import { QueryParam, setUrlParser, Url, UrlMatchPattern } from '../urls'; +import { Variable } from '../variables'; + +describe('test Url object', () => { + setUrlParser(url.URL); + + it('test QueryParam', () => { + const queryParam = new QueryParam({ + key: 'uname', + value: 'patrick star', + }); + + expect(queryParam.toString()).toEqual('uname=patrick+star'); + + queryParam.update('uname=peter+parker'); + expect(queryParam.toString()).toEqual('uname=peter+parker'); + + expect( + QueryParam.unparseSingle({ key: 'uname', value: 'patrick star' }) + ).toEqual('uname=patrick+star'); + + expect( + QueryParam.unparse({ uname: 'patrick star', password: '123' }) + ).toEqual('uname=patrick+star&password=123'); + + expect( + QueryParam.parseSingle('uname=patrick+star') + ).toEqual({ key: 'uname', value: 'patrick star' }); + + expect( + QueryParam.parse('uname=patrick+star&password=123') + ).toEqual([{ 'key': 'uname', 'value': 'patrick star' }, { 'key': 'password', 'value': '123' }]); + }); + + it('test Url methods', () => { + const url = new Url({ + auth: { + username: 'usernameValue', + password: 'passwordValue', + }, + hash: 'hashValue', + host: ['hostValue', 'com'], + path: ['pathLevel1', 'pathLevel2'], + port: '777', + protocol: 'https:', + query: [ + new QueryParam({ key: 'key1', value: 'value1' }), + new QueryParam({ key: 'key2', value: 'value2' }), + ], + variables: [ + new Variable({ key: 'varKey', value: 'varValue' }), + ], + }); + + expect(url.getHost()).toEqual('hostvalue.com'); + expect(url.getPath()).toEqual('/pathLevel1/pathLevel2'); + expect(url.getQueryString()).toEqual('key1=value1&key2=value2'); + expect(url.getPathWithQuery()).toEqual('/pathLevel1/pathLevel2?key1=value1&key2=value2'); + expect(url.getRemote(true)).toEqual('hostvalue.com:777'); + expect(url.getRemote(false)).toEqual('hostvalue.com:777'); // TODO: add more cases + + url.removeQueryParams([ + new QueryParam({ key: 'key1', value: 'value1' }), + ]); + expect(url.getQueryString()).toEqual('key2=value2'); + expect(url.toString()).toEqual('https://usernameValue:passwordValue@hostvalue.com:777/pathLevel1/pathLevel2?key2=value2#hashValue'); + }); + + it('test Url static methods', () => { + // static methods + const urlStr = 'https://myhost.com/path1/path2'; + const urlOptions = Url.parse(urlStr); + const urlObj = new Url(urlOptions || ''); + + expect(urlObj.toString()).toEqual(urlStr); + }); +}); + +describe('test Url Match Pattern', () => { + it('test UrlMatchPattern', () => { + const pattern = 'http+https+custom://*.insomnia.com:80/p1/*'; + const matchPattern = new UrlMatchPattern(pattern); + + expect(matchPattern.getProtocols()).toEqual(['http', 'https', 'custom']); + expect(matchPattern.testProtocol('http')).toBeTruthy(); + expect(matchPattern.testProtocol('https')).toBeTruthy(); + expect(matchPattern.testProtocol('custom')).toBeTruthy(); + expect(matchPattern.testProtocol('unmatched')).toBeFalsy(); + + expect(matchPattern.testHost('download.insomnia.com')).toBeTruthy(); + expect(matchPattern.testHost('bin.download.insomnia.com')).toBeFalsy(); + expect(matchPattern.testHost('insomnia.com')).toBeFalsy(); + expect(matchPattern.testHost('com')).toBeFalsy(); + + expect(matchPattern.testPath('/p1/abc')).toBeTruthy(); + expect(matchPattern.testPath('/p1/')).toBeTruthy(); + expect(matchPattern.testPath('/p1')).toBeFalsy(); + expect(matchPattern.testPath('/')).toBeFalsy(); + expect(matchPattern.testPath('')).toBeFalsy(); + + expect(matchPattern.testPort('80', 'https')).toBeTruthy(); + expect(matchPattern.testPort('443', 'https')).toBeFalsy(); + expect(matchPattern.testPort('80', 'http')).toBeTruthy(); + expect(matchPattern.testPort('80', 'unmatched')).toBeFalsy(); + }); + + it('test UrlMatchPattern with no protocol', () => { + const pattern = '*.insomnia.com/p1/*'; + try { + const matchPattern = new UrlMatchPattern(pattern); + matchPattern.testProtocol('http'); + } catch (e) { + expect(e.message).toContain('UrlMatchPattern: protocol is not specified'); + } + }); + + it('test UrlMatchPattern with no port', () => { + const pattern = 'http+https+custom://*.insomnia.com/p1/*'; + const matchPattern = new UrlMatchPattern(pattern); + + expect(matchPattern.getProtocols()).toEqual(['http', 'https', 'custom']); + expect(matchPattern.testProtocol('http')).toBeTruthy(); + expect(matchPattern.testProtocol('https')).toBeTruthy(); + expect(matchPattern.testProtocol('custom')).toBeTruthy(); + expect(matchPattern.testProtocol('unmatched')).toBeFalsy(); + + expect(matchPattern.testHost('download.insomnia.com')).toBeTruthy(); + expect(matchPattern.testHost('bin.download.insomnia.com')).toBeFalsy(); + expect(matchPattern.testHost('insomnia.com')).toBeFalsy(); + expect(matchPattern.testHost('com')).toBeFalsy(); + + expect(matchPattern.testPath('/p1/abc')).toBeTruthy(); + expect(matchPattern.testPath('/p1/')).toBeTruthy(); + expect(matchPattern.testPath('/p1')).toBeFalsy(); + expect(matchPattern.testPath('/')).toBeFalsy(); + expect(matchPattern.testPath('')).toBeFalsy(); + + expect(matchPattern.testPort('443', 'https')).toBeTruthy(); + expect(matchPattern.testPort('80', 'http')).toBeTruthy(); + expect(matchPattern.testPort('443', 'http')).toBeFalsy(); + expect(matchPattern.testPort('80', 'https')).toBeFalsy(); + }); + + it('test UrlMatchPattern with no path', () => { + const pattern = 'http+https+custom://*.insomnia.com'; + const matchPattern = new UrlMatchPattern(pattern); + + expect(matchPattern.getProtocols()).toEqual(['http', 'https', 'custom']); + expect(matchPattern.testProtocol('http')).toBeTruthy(); + expect(matchPattern.testProtocol('https')).toBeTruthy(); + expect(matchPattern.testProtocol('custom')).toBeTruthy(); + expect(matchPattern.testProtocol('unmatched')).toBeFalsy(); + + expect(matchPattern.testHost('download.insomnia.com')).toBeTruthy(); + expect(matchPattern.testHost('bin.download.insomnia.com')).toBeFalsy(); + expect(matchPattern.testHost('insomnia.com')).toBeFalsy(); + expect(matchPattern.testHost('com')).toBeFalsy(); + + expect(matchPattern.testPath('')).toBeTruthy(); + expect(matchPattern.testPath('/')).toBeFalsy(); // it is not handled temporarily + + expect(matchPattern.testPort('443', 'https')).toBeTruthy(); + expect(matchPattern.testPort('80', 'http')).toBeTruthy(); + expect(matchPattern.testPort('443', 'http')).toBeFalsy(); + expect(matchPattern.testPort('80', 'https')).toBeFalsy(); + }); +}); diff --git a/packages/insomnia/src/sdk/objects/index.ts b/packages/insomnia/src/sdk/objects/index.ts index b25d0a81eb..9349d991ae 100644 --- a/packages/insomnia/src/sdk/objects/index.ts +++ b/packages/insomnia/src/sdk/objects/index.ts @@ -1,4 +1,4 @@ export { PropertyBase, Property, PropertyList } from './properties'; export { Header, HeaderList } from './headers'; export { Variable, VariableList } from './variables'; - +export { QueryParam, Url, UrlMatchPattern, UrlMatchPatternList } from './urls'; diff --git a/packages/insomnia/src/sdk/objects/urls.ts b/packages/insomnia/src/sdk/objects/urls.ts new file mode 100644 index 0000000000..d2a5e4ed52 --- /dev/null +++ b/packages/insomnia/src/sdk/objects/urls.ts @@ -0,0 +1,555 @@ +import { Property, PropertyBase, PropertyList } from './properties'; +import { Variable, VariableList } from './variables'; + +// TODO: make it also work with node.js +let UrlParser = URL; +let UrlSearchParams = URLSearchParams; +export function setUrlParser(provider: any) { + UrlParser = provider; +} +export function setUrlSearchParams(provider: any) { + UrlSearchParams = provider; +} + +export interface QueryParamOptions { + key: string; + value: string; +} + +export class QueryParam extends Property { + _kind: string = 'QueryParam'; + + key: string; + value: string; + + constructor(options: { key: string; value: string } | string) { + super(); + + if (typeof options === 'string') { + try { + const optionsObj = JSON.parse(options); + this.key = optionsObj.key; + this.value = optionsObj.value; + } catch (e) { + throw Error(`invalid QueryParam options ${e}`); + } + } else if (typeof options === 'object' && ('key' in options) && ('value' in options)) { + this.key = options.key; + this.value = options.value; + } else { + throw Error('unknown options for new QueryParam'); + } + } + + // TODO: + // (static) _postman_propertyAllowsMultipleValues :Boolean + // (static) _postman_propertyIndexKey :String + + static parse(queryStr: string) { + const params = new UrlSearchParams(queryStr); + return Array.from(params.entries()) + .map(entry => ({ key: entry[0], value: entry[1] })); + } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + static parseSingle(paramStr: string, _idx?: number, _all?: string[]) { + const pairs = QueryParam.parse(paramStr); + if (pairs.length === 0) { + throw Error('invalid search query string'); + } + + return pairs[0]; + } + + static unparse(params: QueryParamOptions[] | Record) { + const searchParams = new UrlSearchParams(); + + if (Array.isArray(params)) { + params.forEach((entry: QueryParamOptions) => searchParams.append(entry.key, entry.value)); + } else { + Object.entries(params) + .forEach(entry => searchParams.append(entry[0], entry[1])); + } + + return searchParams.toString(); + } + + static unparseSingle(obj: { key: string; value: string }) { + if ('key' in obj && 'value' in obj) { + const params = new UrlSearchParams(); + params.append(obj.key, obj.value); + + return params.toString(); + } + return {}; + } + + toString() { + const params = new UrlSearchParams(); + params.append(this.key, this.value); + + return params.toString(); + } + + update(param: string | { key: string; value: string }) { + if (typeof param === 'string') { + const paramObj = QueryParam.parseSingle(param); + this.key = typeof paramObj.key === 'string' ? paramObj.key : ''; + this.value = typeof paramObj.value === 'string' ? paramObj.value : ''; + } else if ('key' in param && 'value' in param) { + this.key = param.key; + this.value = param.value; + } else { + throw Error('the param for update must be: string | { key: string; value: string }'); + } + } +} + +export interface UrlOptions { + auth?: { + username: string; + password: string; + }; + hash?: string; + host: string[]; + path?: string[]; + port?: string; + protocol: string; + query: { key: string; value: string }[]; + variables: { key: string; value: string }[]; +} + +export class Url extends PropertyBase { + _kind: string = 'Url'; + + // TODO: should be related to RequestAuth + // but the implementation seems only supports username + password + auth?: { username: string; password: string }; + hash?: string; + host: string[] = []; + path?: string[] = []; + port?: string; + protocol?: string; + query: PropertyList = new PropertyList(QueryParam, undefined, []); + variables: VariableList = new VariableList(undefined, []); + + constructor( + def: UrlOptions | string + ) { + super(); + this.setFields(def); + } + + private setFields(def: UrlOptions | string) { + const urlObj = typeof def === 'string' ? Url.parse(def) : def; + + if (urlObj) { + this.auth = urlObj.auth; + this.hash = urlObj.hash; + this.host = urlObj.host; + this.path = urlObj.path; + this.port = urlObj.port; + this.protocol = urlObj.protocol; + + const queryList = urlObj.query ? + urlObj.query.map(kvObj => new QueryParam(kvObj), {}) : + []; + this.query = new PropertyList( + QueryParam, + undefined, + queryList + ); + + // TODO: variable is always empty in this way + const varList = urlObj.variables ? + urlObj.variables + .map( + (kvObj: { key: string; value: string }) => new Variable(kvObj), + {}, + ) : + []; + + this.variables = new VariableList(undefined, varList); + + } else { + throw Error(`url is invalid: ${def}`); // TODO: + } + } + + static isUrl(obj: object) { + return '_kind' in obj && obj._kind === 'Url'; + } + + static parse(urlStr: string): UrlOptions | undefined { + // TODO: enable validation + // if (!UrlParser.canParse(urlStr)) { + // console.error(`invalid URL string ${urlStr}`); + // return undefined; + // } + + const url = new UrlParser(urlStr); + const query = Array.from(url.searchParams.entries()) + .map(kv => { + const kvArray = kv as [string, string]; + return { key: kvArray[0], value: kvArray[1] }; + }); + + return { + auth: url.username !== '' ? { // TODO: make it compatible with RequestAuth + username: url.username, + password: url.password, + } : undefined, + hash: url.hash, + host: url.hostname.split('/'), + path: url.pathname.split('/'), + port: url.port, + protocol: url.protocol, // e.g. https: + query, + variables: [], + }; + } + + addQueryParams(params: { key: string; value: string }[] | string) { + let queryParams: { key: string; value: string }[]; + + if (typeof params === 'string') { + queryParams = QueryParam.parse(params); + } else { + queryParams = params; + } + + queryParams.forEach((param: { key: string; value: string }) => { + this.query.append(new QueryParam({ key: param.key, value: param.value })); + }); + } + + getHost() { + return this.host.join('.').toLowerCase(); + } + + getPath(unresolved?: boolean) { + const pathStr = this.path ? this.path.join('/') : '/'; + const finalPath = pathStr.startsWith('/') ? pathStr : '/' + pathStr; + + if (unresolved) { + return finalPath; + } + + // TODO: should it support rendering variables here? + return finalPath; + } + + getPathWithQuery() { + return `${this.getPath(true)}?${this.getQueryString()}`; + } + + getQueryString() { + const params = new UrlSearchParams(); + this.query.each(param => params.append(param.key, param.value), {}); + + return params.toString(); + } + + getRemote(forcePort?: boolean) { + const host = this.getHost(); + + if (forcePort) { + // TODO: it does not support GQL, gRPC and so on + const port = this.port ? this.port : + this.protocol && (this.protocol === 'https:') ? 443 : 80; + return `${host}:${port}`; + } + + // TODO: it does not support GQL, gRPC and so on + const portWithColon = this.port ? `:${this.port}` : ''; + return `${host}${portWithColon}`; + } + + removeQueryParams(params: QueryParam[] | string[] | string) { + if (typeof params === 'string') { + // it is a string + this.query = new PropertyList( + QueryParam, + undefined, + this.query.filter(queryParam => queryParam.key === params, {}) + ); + } else if (params.length > 0) { + let toBeRemoved: Set; + + if (typeof params[0] === 'string') { + // it is a string[] + toBeRemoved = new Set(params as string[]); + } else { + // it is a QueryParam[] + toBeRemoved = new Set( + (params as QueryParam[]) + .map(param => param.key) + ); + } + + this.query = new PropertyList( + QueryParam, + undefined, + this.query.filter(queryParam => !toBeRemoved.has(queryParam.key), {}) + ); + } else { + console.error('failed to remove query params: unknown params type, only supports QueryParam[], string[] or string'); + } + } + + toString(forceProtocol?: boolean) { + const protocol = forceProtocol ? + (this.protocol ? this.protocol : 'https:') : + (this.protocol ? this.protocol : ''); + + const parser = new UrlParser(`${protocol}//` + this.getHost()); + parser.username = this.auth?.username || ''; + parser.password = this.auth?.password || ''; + parser.port = this.port || ''; + parser.pathname = this.getPath(); + // eslint-disable-next-line @typescript-eslint/no-unused-vars + parser.search = this.getQueryString(); + parser.hash = this.hash || ''; + + return parser.toString(); + } + + update(url: UrlOptions | string) { + this.setFields(url); + } +} + +// interface Matcher { +// match(pattern: string): boolean; +// } + +// UrlMatchPattern implements chrome extension match patterns: +// https://developer.chrome.com/docs/extensions/develop/concepts/match-patterns +export class UrlMatchPattern extends Property { + // scheme + // scheme: 'http:' | 'https:' | '*' | 'file:'; + + // host + // About wildcard: + // If you use a wildcard in the host pattern + // it must be the first or only character, and it must be followed by a period (.) or forward slash (/). + + // path + // Must contain at least a forward slash + // The slash by itself matches any path. + + // Special cases: https://developer.chrome.com/docs/extensions/develop/concepts/match-patterns#special + // "" + // "file:///" + // "http://localhost/*" + // It doesn't support match patterns for top Level domains (TLD). + + private pattern: string; + + constructor(pattern: string) { + super(); + + this.pattern = pattern; + } + + static readonly MATCH_ALL_URLS: string = ''; + static pattern: string | undefined = undefined; // TODO: its usage is unknown + static readonly PROTOCOL_DELIMITER: string = '+'; + + // TODO: the url can not start with - + + getProtocols(): string[] { + const protocolEndPos = this.pattern.indexOf('://'); + if (protocolEndPos < 0) { + throw Error('UrlMatchPattern: protocol is not specified'); + } + + const protocolPattern = this.pattern.slice(0, protocolEndPos); + const protocols = protocolPattern.split(UrlMatchPattern.PROTOCOL_DELIMITER); + + return protocols.map(protocol => protocol.replace(':', '')); + } + + test(urlStr: string) { + const protoEndPos = urlStr.indexOf(':'); + const protoStr = urlStr.slice(0, protoEndPos); + const hostStr = this.getHost(urlStr); + const pathStr = this.getPath(this.pattern); + const portStr = this.getPort(urlStr); + + return this.testProtocol(protoStr) && + this.testHost(hostStr) && + this.testPath(pathStr) && + this.testPort(portStr, protoStr); + } + + private getHost(urlStr: string) { + const protocolEndPos = urlStr.indexOf('://') + 3; + const hostBegPos = protocolEndPos; + + const portBegPos = urlStr.indexOf(':', protocolEndPos); + const pathBegPos = urlStr.indexOf('/', protocolEndPos); + const queryBegPos = urlStr.indexOf('?', protocolEndPos); + const hashBegPos = urlStr.indexOf('?', protocolEndPos); + + let hostEndPos = urlStr.length; + if (portBegPos >= 0) { + hostEndPos = portBegPos; + } else if (pathBegPos >= 0) { + hostEndPos = pathBegPos; + } else if (queryBegPos >= 0) { + hostEndPos = queryBegPos; + } else if (hashBegPos >= 0) { + hostEndPos = hashBegPos; + } + + return urlStr.slice(hostBegPos, hostEndPos); + } + + testHost(hostStr: string) { + const patternSegments = this.getHost(this.pattern).split('.'); + console.log(patternSegments); + + const inputHostSegments = hostStr.split('.'); + + if (patternSegments.length !== inputHostSegments.length) { + return false; + } + + for (let i = 0; i < patternSegments.length; i++) { + if (patternSegments[i] === '*') { + continue; + } else if (patternSegments[i] !== inputHostSegments[i]) { + return false; + } + } + return true; + } + + private getPath(urlStr: string) { + const protocolEndPos = urlStr.indexOf('://') + 3; + const hostBegPos = protocolEndPos; + const pathBegPos = urlStr.indexOf('/', hostBegPos); + if (pathBegPos < 0) { + return ''; + } + + const queryBegPos = urlStr.indexOf('?'); + const hashBegPos = urlStr.indexOf('#'); + let pathEndPos = urlStr.length; + if (queryBegPos >= 0) { + pathEndPos = queryBegPos; + } else if (hashBegPos >= 0) { + pathEndPos = hashBegPos; + } + + return urlStr.slice(pathBegPos, pathEndPos); + } + + testPath(pathStr: string) { + const patternSegments = this.getPath(this.pattern).split('/'); + const inputSegments = pathStr.split('/'); + + if (patternSegments.length !== inputSegments.length) { + return false; + } + + for (let i = 0; i < patternSegments.length; i++) { + if (patternSegments[i] === '*') { + continue; + } else if (patternSegments[i] !== inputSegments[i]) { + return false; + } + } + return true; + } + + private getPort(urlStr: string) { + const protocolEndPos = urlStr.indexOf('/') + 2; + const hostBegPos = protocolEndPos; + + const portBegPos = this.pattern.indexOf(':', protocolEndPos) + 1; + if (portBegPos <= 0) { + return ''; + } + + let portEndPos = this.pattern.length; + const pathBegPos = urlStr.indexOf('/', hostBegPos); + const queryBegPos = urlStr.indexOf('?'); + const hashBegPos = urlStr.indexOf('#'); + + if (pathBegPos >= 0) { + portEndPos = pathBegPos; + } else if (queryBegPos >= 0) { + portEndPos = queryBegPos; + } else if (hashBegPos >= 0) { + portEndPos = hashBegPos; + } + + return urlStr.slice(portBegPos, portEndPos); + } + + testPort(port: string, protocol: string) { + if (!this.testProtocol(protocol)) { + return false; + } + + const portPattern = this.getPort(this.pattern); + if (portPattern === '*') { + return true; + } else if (portPattern === '') { + const protos = this.getProtocols(); + + if (protos.includes('https') && port === '443' && protocol === 'https') { + return true; + } else if (protos.includes('http') && port === '80' && protocol === 'http') { + return true; + } + } + + return portPattern === port; + } + + testProtocol(protocol: string) { + const protoPatterns = this.getProtocols(); + + for (let i = 0; i < protoPatterns.length; i++) { + if (protoPatterns[i] === '*') { + return true; + } else if (protoPatterns[i] === protocol) { + return true; + } + } + return false; + } + + toString() { + return this.pattern; + } + + update(pattern: string) { + this.pattern = pattern; + } +} + +export class UrlMatchPatternList extends PropertyList { + _kind: string = 'UrlMatchPatternList'; + + constructor(parent: PropertyList | undefined, populate: T[]) { + super(UrlMatchPattern, undefined, populate); + this.parent = parent; + } + + static isUrlMatchPatternList(obj: any) { + return '_kind' in obj && obj._kind === 'UrlMatchPatternList'; + } + + // TODO: unsupported yet + // toObject(excludeDisabledopt, nullable, caseSensitiveopt, nullable, multiValueopt, nullable, sanitizeKeysopt) → {Object} + + test(urlStr: string) { + return this + .filter(matchPattern => matchPattern.test(urlStr), {}) + .length > 0; + } +}