From 392a3920df6d78981ac43741f15048c84102b046 Mon Sep 17 00:00:00 2001 From: Xiaozhen Liu Date: Wed, 20 May 2020 17:47:33 -0700 Subject: [PATCH] feat: parse path template using regexes (#823) * get rid of pegjs * clean up * comments * remove pegjs dependency * error messages * feedback --- package.json | 3 +- src/parserExtras.ts | 97 ----- src/pathTemplate.ts | 269 ++++++++----- src/pathTemplateParser.js | 680 -------------------------------- src/pathTemplateParser.pegjs | 86 ---- test/unit/pathTemplate.ts | 4 +- test/unit/pathTemplateParser.ts | 59 --- 7 files changed, 163 insertions(+), 1035 deletions(-) delete mode 100644 src/parserExtras.ts delete mode 100644 src/pathTemplateParser.js delete mode 100644 src/pathTemplateParser.pegjs delete mode 100644 test/unit/pathTemplateParser.ts diff --git a/package.json b/package.json index 99e758d88..4803c78ff 100644 --- a/package.json +++ b/package.json @@ -68,7 +68,6 @@ "mocha": "^7.0.0", "ncp": "^2.0.0", "null-loader": "^4.0.0", - "pegjs": "~0.10.0", "proxyquire": "^2.0.1", "pumpify": "^2.0.0", "puppeteer": "^2.0.0", @@ -88,7 +87,7 @@ "test": "c8 mocha build/test/unit", "lint": "gts check", "clean": "gts clean", - "compile": "tsc -p . && cp src/*.json build/src && cp src/*.js build/src && cp -r test/fixtures build/test", + "compile": "tsc -p . && cp src/*.json build/src && cp -r test/fixtures build/test", "compile-operation-protos": "pbjs -t json google/longrunning/operations.proto -p ./protos > protos/operations.json && pbjs -t json google/rpc/status.proto google/rpc/error_details.proto google/protobuf/any.proto -p ./protos > protos/status.json && pbjs -t static-module google/longrunning/operations.proto -p ./protos > protos/operations.js && pbts protos/operations.js -o protos/operations.d.ts", "compile-iam-protos": "pbjs -t json google/iam/v1/iam_policy.proto google/iam/v1/options.proto google/iam/v1/policy.proto google/iam/v1/logging/audit_data.proto -p ./protos > protos/iam_service.json && pbjs -t static-module google/iam/v1/iam_policy.proto google/iam/v1/options.proto google/iam/v1/policy.proto google/iam/v1/logging/audit_data.proto -p ./protos > protos/iam_service.js && pbts protos/iam_service.js -o protos/iam_service.d.ts", "fix": "gts fix", diff --git a/src/parserExtras.ts b/src/parserExtras.ts deleted file mode 100644 index 1008c067d..000000000 --- a/src/parserExtras.ts +++ /dev/null @@ -1,97 +0,0 @@ -/** - * Copyright 2020 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import * as util from 'util'; -import {Segment} from './pathTemplate'; - -/* constants used in the pegjs parser */ -export const BINDING = 1; -export const END_BINDING = 2; -export const TERMINAL = 3; - -/** - * Checks that segments only has one terminal segment that is a path wildcard. - * - * @private - * - * @param {Segments[]} segments the parsed segments - * @throws {TypeError} if there are too many - */ -function allowOnePathWildcard(segments: Segment[]) { - let hasPathWildcard = false; - for (let i = 0; i < segments.length; i++) { - const s = segments[i]; - if (s.kind !== TERMINAL || s.literal !== '**') { - continue; - } - if (hasPathWildcard) { - const tooManyWildcards = 'cannot contain more than one path wildcard'; - throw new TypeError(tooManyWildcards); - } - hasPathWildcard = true; - } -} - -/** - * Counts the number of terminal segments. - * - * @private - * - * @param {Segments[]} segments the parsed segments - * @return {number} the number of terminal segments in the template - */ -function countTerminals(segments: Segment[]) { - return segments.filter(x => x.kind === TERMINAL).length; -} - -/** - * Updates missing literals of each of the binding segments. - * - * @private - * - * @param {Segments[]} segments the parsed segments - */ -function updateBindingLiterals(segments: Segment[]) { - let bindingIndex = 0; - segments.forEach(s => { - if (s.kind === BINDING && !s.literal) { - s.literal = util.format('$%d', bindingIndex); - bindingIndex += 1; - } - }); -} - -/** - * Completes the parsing of the segments - * - * Validates them, and transforms them into the object used by the - * PathTemplate class. - * - * @private - * - * @param {Segments[]} segments the parsed segments - * @param {Object} initializes the attributes of a PathTemplate - * @return {Object} Returns segments and size - * @throws {TypeError} if multiple path wildcards exist - */ -export function finishParse(segments: Segment[]) { - allowOnePathWildcard(segments); - updateBindingLiterals(segments); - return { - segments, - size: countTerminals(segments), - }; -} diff --git a/src/pathTemplate.ts b/src/pathTemplate.ts index a08d635eb..563318787 100644 --- a/src/pathTemplate.ts +++ b/src/pathTemplate.ts @@ -18,44 +18,24 @@ * Path template utility. */ -import has = require('lodash.has'); -import * as util from 'util'; -import * as extras from './parserExtras'; -// eslint-disable-next-line @typescript-eslint/no-var-requires -const parser = require('./pathTemplateParser'); - -export interface ParseResult { - size: number; - segments: Segment[]; -} - -export interface Segment { - kind: number; - literal: string; -} - export interface Bindings { [index: string]: string | number; } export class PathTemplate { - private readonly parseResult: ParseResult; - - get size(): number { - return this.parseResult.size; - } - - get segments(): Segment[] { - return this.parseResult.segments; - } - + private data: string; + private bindings: Bindings = {}; + segments: string[]; + size: number; /** * @param {String} data the of the template * * @constructor */ constructor(data: string) { - this.parseResult = extras.finishParse(parser.parse(data)); + this.data = data; + this.segments = this.parsePathTemplate(data); + this.size = this.segments.length; } /** @@ -66,45 +46,45 @@ export class PathTemplate { * @throws {TypeError} if path can't be matched to this template */ match(path: string): Bindings { - const pathSegments = path.split('/'); + let pathSegments = path.split('/'); + const bindings: Bindings = {}; - let segmentCount = this.size; - let current: string; - let index = 0; - this.segments.forEach(segment => { - if (index > pathSegments.length) { - return; + if (pathSegments.length !== this.segments.length) { + // if the path contains a wildcard, then the length may differ by 1. + if (!this.data.includes('**')) { + throw new TypeError( + `This path ${path} does not match path template ${this.data}, the number of parameters is not same.` + ); + } else if (pathSegments.length !== this.segments.length + 1) { + throw new TypeError( + `This path ${path} does not match path template ${this.data}, the number of parameters is not same with one wildcard.` + ); } - if (segment.kind === extras.BINDING) { - current = segment.literal; - } else if (segment.kind === extras.TERMINAL) { - if (segment.literal === '*') { - bindings[current] = pathSegments[index]; - index += 1; - } else if (segment.literal === '**') { - const size = pathSegments.length - segmentCount + 1; - segmentCount += size - 1; - bindings[current] = pathSegments.slice(index, index + size).join('/'); - index += size; - } else if (segment.literal === pathSegments[index]) { - index += 1; - } else { - const msg = util.format( - "mismatched literal (index=%d): '%s' != '%s'", - index, - segment.literal, - pathSegments[index] + } + for ( + let index = 0; + index < this.segments.length && pathSegments.length > 0; + index++ + ) { + if (this.segments[index] !== pathSegments[0]) { + if (!this.segments[index].includes('*')) { + throw new TypeError( + `segment does not match, ${this.segments[index]} and ${pathSegments[index]}.` ); - throw new TypeError(msg); + } else { + const segment = this.segments[index]; + const variable = segment.match(/(?<={)[$0-9a-zA-Z_]+(?==.*?})/g); + if (this.segments[index].includes('**')) { + bindings[variable![0]] = pathSegments[0] + '/' + pathSegments[1]; + pathSegments = pathSegments.slice(2); + } else { + bindings[variable![0]] = pathSegments[0]; + pathSegments.shift(); + } } + } else { + pathSegments.shift(); } - }); - if (index !== pathSegments.length || index !== segmentCount) { - const msg = util.format( - 'match error: could not instantiate a path template from %s', - path - ); - throw new TypeError(msg); } return bindings; } @@ -118,33 +98,36 @@ export class PathTemplate { * parsed */ render(bindings: Bindings): string { - const out: Segment[] = []; - let inABinding = false; - this.segments.forEach(segment => { - if (segment.kind === extras.BINDING) { - if (!has(bindings, segment.literal)) { - const msg = util.format( - 'Value for key %s is not provided in %s', - segment.literal, - bindings - ); - throw new TypeError(msg); - } - const tmp = new PathTemplate(bindings[segment.literal].toString()); - Array.prototype.push.apply(out, tmp.segments); - inABinding = true; - } else if (segment.kind === extras.END_BINDING) { - inABinding = false; - } else if (inABinding) { - return; - } else { - out.push(segment); + if (Object.keys(bindings).length !== Object.keys(this.bindings).length) { + throw new TypeError( + `The number of variables ${ + Object.keys(bindings).length + } does not match the number of needed variables ${ + Object.keys(this.bindings).length + }` + ); + } + let path = this.inspect(); + for (const key of Object.keys(bindings)) { + const b = bindings[key].toString(); + if (!this.bindings[key]) { + throw new TypeError(`render fails for not matching ${bindings[key]}`); } - }); + const variable = this.bindings[key]; - const result = formatSegments(out); - this.match(result); - return result; + if (variable === '*') { + if (!b.match(/[^/{}]+/)) { + throw new TypeError(`render fails for not matching ${b}`); + } + path = path.replace(`{${key}=*}`, `${b}`); + } else if (variable === '**') { + if (!b.match(/[^{}]+/)) { + throw new TypeError(`render fails for not matching ${b}`); + } + path = path.replace(`{${key}=**}`, `${b}`); + } + } + return path; } /** @@ -152,35 +135,103 @@ export class PathTemplate { * * @return {string} contains const names matched to binding values */ - inspect() { - return formatSegments(this.segments); + inspect(): string { + return this.segments.join('/'); + } + /** + * Parse the path template. + * + * @return {string[]} return segments of the input path. + * For example: 'buckets/{hello}'' will give back ['buckets', {hello=*}] + */ + private parsePathTemplate(data: string): string[] { + const pathSegments = splitPathTemplate(data); + let index = 0; + let wildCardCount = 0; + const segments: string[] = []; + pathSegments.forEach(segment => { + // * or ** -> segments.push('{$0=*}'); + // -> bindings['$0'] = '*' + if (segment === '*' || segment === '**') { + this.bindings[`$${index}`] = segment; + segments.push(`{$${index}=${segment}}`); + index = index + 1; + if (segment === '**') { + wildCardCount = wildCardCount + 1; + if (wildCardCount > 1) { + throw new TypeError('Can not have more than one wildcard.'); + } + } + } + // {project} / {project=*} -> segments.push('{project=*}'); + // -> bindings['project'] = '*' + else if (segment.match(/(?<={)[0-9a-zA-Z-.~_]+(=\*)?(?=})/)) { + const variable = segment.match(/(?<={)[0-9a-zA-Z-.~_]+(=\*)?(?=})/); + this.bindings[variable![0]] = '*'; + segments.push(`{${variable![0]}=*}`); + } + // {hello=/what} -> segments.push('{hello=/what}'); + // -> no binding in this case + else if (segment.match(/(?<={)[0-9a-zA-Z-.~_]+=[^*]+(?=})/)) { + segments.push(segment); + } + // helloazAZ09-.~_what -> segments.push('helloazAZ09-.~_what'); + // -> no binding in this case + else if (segment.match(/[0-9a-zA-Z-.~_]+/)) { + segments.push(segment); + } + // {project}~{location} -> {project=*}~{location=*} + else if ( + segment.match( + /(?<={)[0-9a-zA-Z-.~_]+(?:}[-._~]?{)[0-9a-zA-Z-.~_]+(?=})/ + ) + ) { + // [project, location] + const variable = segment.match(/(?<=\{).*?(?=(?:=.*?)?\})/g); + variable?.forEach(v => { + this.bindings[v] = '*'; + segment.replace(v, v + '=*'); + }); + segments.push(segment); + } + }); + return segments; } } /** - * Creates the string representattion for the segments. - * @param {Object[]} segments - The array of segments. - * @return {string} - A string representing segments in the path template - * format. + * Split the path template by `/`. + * It can not be simply splitted by `/` because there might be `/` in the segments. + * For example: 'a/b/{a=hello/world}' we do not want to break the brackets pair + * so above path will be splitted as ['a', 'b', '{a=hello/world}'] */ -function formatSegments(segments: Segment[]): string { - let out = ''; - let slash = true; - segments.forEach(segment => { - if (segment.kind === extras.TERMINAL) { - if (slash) { - out += '/'; +function splitPathTemplate(data: string): string[] { + let left = 0; + let right = 0; + let bracketCount = 0; + const segments: string[] = []; + while (right >= left && right < data.length) { + if (data.charAt(right) === '{') { + bracketCount = bracketCount + 1; + } else if (data.charAt(right) === '}') { + bracketCount = bracketCount - 1; + } else if (data.charAt(right) === '/') { + if (right === data.length - 1) { + throw new TypeError('Invalid path, it can not be ended by /'); + } + if (bracketCount === 0) { + // complete bracket, to avoid the case a/b/**/*/{a=hello/world} + segments.push(data.substring(left, right)); + left = right + 1; } - out += segment.literal; - return; } - slash = true; - if (segment.kind === extras.BINDING) { - out += '/{' + segment.literal + '='; - slash = false; - } else { - out += segment.literal + '}'; + if (right === data.length - 1) { + if (bracketCount !== 0) { + throw new TypeError('Brackets are invalid.'); + } + segments.push(data.substring(left)); } - }); - return out.substring(1); + right = right + 1; + } + return segments; } diff --git a/src/pathTemplateParser.js b/src/pathTemplateParser.js deleted file mode 100644 index 434a5f0a1..000000000 --- a/src/pathTemplateParser.js +++ /dev/null @@ -1,680 +0,0 @@ -/** - * Copyright 2020 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -// This was an auto-generated file, we disable all linter checks for this file. -/* eslint-disable */ - -module.exports = (() => { - /* - * Generated by PEG.js 0.9.0. - * - * http://pegjs.org/ - */ - - function peg$subclass(child, parent) { - function ctor() { - this.constructor = child; - } - ctor.prototype = parent.prototype; - child.prototype = new ctor(); - } - - function peg$SyntaxError(message, expected, found, location) { - this.message = message; - this.expected = expected; - this.found = found; - this.location = location; - this.name = "SyntaxError"; - - if (typeof Error.captureStackTrace === "function") { - Error.captureStackTrace(this, peg$SyntaxError); - } - } - - peg$subclass(peg$SyntaxError, Error); - - function peg$parse(input) { - const options = arguments.length > 1 ? arguments[1] : {}; - const parser = this; - const peg$FAILED = {}; - const peg$startRuleFunctions = { template: peg$parsetemplate }; - let peg$startRuleFunction = peg$parsetemplate; - const peg$c0 = "/"; - const peg$c1 = { type: "literal", value: "/", description: '"/"' }; - const peg$c2 = segments => { - return segments; - }; - const peg$c3 = (s, segments) => { - return s.concat(segments); - }; - const peg$c4 = s => { - return s; - }; - const peg$c5 = "{"; - const peg$c6 = { type: "literal", value: "{", description: '"{"' }; - const peg$c7 = "="; - const peg$c8 = { type: "literal", value: "=", description: '"="' }; - const peg$c9 = "}"; - const peg$c10 = { type: "literal", value: "}", description: '"}"' }; - const peg$c11 = (l, segments) => { - return [ - { kind: extras.BINDING, literal: l }, - segments, - { kind: extras.END_BINDING, literal: "" } - ].reduce((a, b) => a.concat(b), []); - }; - const peg$c12 = l => { - return [ - { kind: extras.BINDING, literal: l }, - { kind: extras.TERMINAL, literal: "*" }, - { kind: extras.END_BINDING, literal: "" } - ]; - }; - const peg$c13 = (t, segments) => { - return t.concat(segments); - }; - const peg$c14 = t => { - if (t[0].literal === "*" || t[0].literal === "**") { - return [ - { - kind: extras.BINDING - }, - t[0], - { kind: extras.END_BINDING, literal: "" } - ]; - } else { - return t; - } - }; - const peg$c15 = "**"; - const peg$c16 = { type: "literal", value: "**", description: '"**"' }; - const peg$c17 = "*"; - const peg$c18 = { type: "literal", value: "*", description: '"*"' }; - const peg$c19 = l => { - return [{ kind: extras.TERMINAL, literal: l }]; - }; - const peg$c20 = /^[^*=}{\/]/; - const peg$c21 = { - type: "class", - value: "[^*=}{/]", - description: "[^*=}{/]" - }; - const peg$c22 = cs => { - return cs.join(""); - }; - let peg$currPos = 0; - let peg$savedPos = 0; - const peg$posDetailsCache = [{ line: 1, column: 1, seenCR: false }]; - let peg$maxFailPos = 0; - let peg$maxFailExpected = []; - const peg$silentFails = 0; - let peg$result; - - if ("startRule" in options) { - if (!(options.startRule in peg$startRuleFunctions)) { - throw new Error( - "Can't start parsing from rule \"" + options.startRule + '".' - ); - } - - peg$startRuleFunction = peg$startRuleFunctions[options.startRule]; - } - - function text() { - return input.substring(peg$savedPos, peg$currPos); - } - - function location() { - return peg$computeLocation(peg$savedPos, peg$currPos); - } - - function expected(description) { - throw peg$buildException( - null, - [{ type: "other", description }], - input.substring(peg$savedPos, peg$currPos), - peg$computeLocation(peg$savedPos, peg$currPos) - ); - } - - function error(message) { - throw peg$buildException( - message, - null, - input.substring(peg$savedPos, peg$currPos), - peg$computeLocation(peg$savedPos, peg$currPos) - ); - } - - function peg$computePosDetails(pos) { - let details = peg$posDetailsCache[pos], - p, - ch; - - if (details) { - return details; - } else { - p = pos - 1; - while (!peg$posDetailsCache[p]) { - p--; - } - - details = peg$posDetailsCache[p]; - details = { - line: details.line, - column: details.column, - seenCR: details.seenCR - }; - - while (p < pos) { - ch = input.charAt(p); - if (ch === "\n") { - if (!details.seenCR) { - details.line++; - } - details.column = 1; - details.seenCR = false; - } else if (ch === "\r" || ch === "\u2028" || ch === "\u2029") { - details.line++; - details.column = 1; - details.seenCR = true; - } else { - details.column++; - details.seenCR = false; - } - - p++; - } - - peg$posDetailsCache[pos] = details; - return details; - } - } - - function peg$computeLocation(startPos, endPos) { - const startPosDetails = peg$computePosDetails(startPos), - endPosDetails = peg$computePosDetails(endPos); - - return { - start: { - offset: startPos, - line: startPosDetails.line, - column: startPosDetails.column - }, - end: { - offset: endPos, - line: endPosDetails.line, - column: endPosDetails.column - } - }; - } - - function peg$fail(expected) { - if (peg$currPos < peg$maxFailPos) { - return; - } - - if (peg$currPos > peg$maxFailPos) { - peg$maxFailPos = peg$currPos; - peg$maxFailExpected = []; - } - - peg$maxFailExpected.push(expected); - } - - function peg$buildException(message, expected, found, location) { - function cleanupExpected(expected) { - let i = 1; - - expected.sort((a, b) => { - if (a.description < b.description) { - return -1; - } else if (a.description > b.description) { - return 1; - } else { - return 0; - } - }); - - while (i < expected.length) { - if (expected[i - 1] === expected[i]) { - expected.splice(i, 1); - } else { - i++; - } - } - } - - function buildMessage(expected, found) { - function stringEscape(s) { - function hex(ch) { - return ch - .charCodeAt(0) - .toString(16) - .toUpperCase(); - } - - return s - .replace(/\\/g, "\\\\") - .replace(/"/g, '\\"') - .replace(/\x08/g, "\\b") - .replace(/\t/g, "\\t") - .replace(/\n/g, "\\n") - .replace(/\f/g, "\\f") - .replace(/\r/g, "\\r") - .replace(/[\x00-\x07\x0B\x0E\x0F]/g, ch => { - return "\\x0" + hex(ch); - }) - .replace(/[\x10-\x1F\x80-\xFF]/g, ch => { - return "\\x" + hex(ch); - }) - .replace(/[\u0100-\u0FFF]/g, ch => { - return "\\u0" + hex(ch); - }) - .replace(/[\u1000-\uFFFF]/g, ch => { - return "\\u" + hex(ch); - }); - } - - const expectedDescs = new Array(expected.length); - let expectedDesc, foundDesc, i; - - for (i = 0; i < expected.length; i++) { - expectedDescs[i] = expected[i].description; - } - - expectedDesc = - expected.length > 1 - ? expectedDescs.slice(0, -1).join(", ") + - " or " + - expectedDescs[expected.length - 1] - : expectedDescs[0]; - - foundDesc = found ? '"' + stringEscape(found) + '"' : "end of input"; - - return "Expected " + expectedDesc + " but " + foundDesc + " found."; - } - - if (expected !== null) { - cleanupExpected(expected); - } - - return new peg$SyntaxError( - message !== null ? message : buildMessage(expected, found), - expected, - found, - location - ); - } - - function peg$parsetemplate() { - let s0, s1, s2; - - s0 = peg$currPos; - if (input.charCodeAt(peg$currPos) === 47) { - s1 = peg$c0; - peg$currPos++; - } else { - s1 = peg$FAILED; - if (peg$silentFails === 0) { - peg$fail(peg$c1); - } - } - if (s1 !== peg$FAILED) { - s2 = peg$parsebound_segments(); - if (s2 !== peg$FAILED) { - peg$savedPos = s0; - s1 = peg$c2(s2); - s0 = s1; - } else { - peg$currPos = s0; - s0 = peg$FAILED; - } - } else { - peg$currPos = s0; - s0 = peg$FAILED; - } - if (s0 === peg$FAILED) { - s0 = peg$currPos; - s1 = peg$parsebound_segments(); - if (s1 !== peg$FAILED) { - peg$savedPos = s0; - s1 = peg$c2(s1); - } - s0 = s1; - } - - return s0; - } - - function peg$parsebound_segments() { - let s0, s1, s2, s3; - - s0 = peg$currPos; - s1 = peg$parsebound_segment(); - if (s1 !== peg$FAILED) { - if (input.charCodeAt(peg$currPos) === 47) { - s2 = peg$c0; - peg$currPos++; - } else { - s2 = peg$FAILED; - if (peg$silentFails === 0) { - peg$fail(peg$c1); - } - } - if (s2 !== peg$FAILED) { - s3 = peg$parsebound_segments(); - if (s3 !== peg$FAILED) { - peg$savedPos = s0; - s1 = peg$c3(s1, s3); - s0 = s1; - } else { - peg$currPos = s0; - s0 = peg$FAILED; - } - } else { - peg$currPos = s0; - s0 = peg$FAILED; - } - } else { - peg$currPos = s0; - s0 = peg$FAILED; - } - if (s0 === peg$FAILED) { - s0 = peg$parsebound_segment(); - } - - return s0; - } - - function peg$parsebound_segment() { - let s0, s1; - - s0 = peg$currPos; - s1 = peg$parsebound_terminal(); - if (s1 === peg$FAILED) { - s1 = peg$parsevariable(); - } - if (s1 !== peg$FAILED) { - peg$savedPos = s0; - s1 = peg$c4(s1); - } - s0 = s1; - - return s0; - } - - function peg$parsevariable() { - let s0, s1, s2, s3, s4, s5; - - s0 = peg$currPos; - if (input.charCodeAt(peg$currPos) === 123) { - s1 = peg$c5; - peg$currPos++; - } else { - s1 = peg$FAILED; - if (peg$silentFails === 0) { - peg$fail(peg$c6); - } - } - if (s1 !== peg$FAILED) { - s2 = peg$parseliteral(); - if (s2 !== peg$FAILED) { - if (input.charCodeAt(peg$currPos) === 61) { - s3 = peg$c7; - peg$currPos++; - } else { - s3 = peg$FAILED; - if (peg$silentFails === 0) { - peg$fail(peg$c8); - } - } - if (s3 !== peg$FAILED) { - s4 = peg$parseunbound_segments(); - if (s4 !== peg$FAILED) { - if (input.charCodeAt(peg$currPos) === 125) { - s5 = peg$c9; - peg$currPos++; - } else { - s5 = peg$FAILED; - if (peg$silentFails === 0) { - peg$fail(peg$c10); - } - } - if (s5 !== peg$FAILED) { - peg$savedPos = s0; - s1 = peg$c11(s2, s4); - s0 = s1; - } else { - peg$currPos = s0; - s0 = peg$FAILED; - } - } else { - peg$currPos = s0; - s0 = peg$FAILED; - } - } else { - peg$currPos = s0; - s0 = peg$FAILED; - } - } else { - peg$currPos = s0; - s0 = peg$FAILED; - } - } else { - peg$currPos = s0; - s0 = peg$FAILED; - } - if (s0 === peg$FAILED) { - s0 = peg$currPos; - if (input.charCodeAt(peg$currPos) === 123) { - s1 = peg$c5; - peg$currPos++; - } else { - s1 = peg$FAILED; - if (peg$silentFails === 0) { - peg$fail(peg$c6); - } - } - if (s1 !== peg$FAILED) { - s2 = peg$parseliteral(); - if (s2 !== peg$FAILED) { - if (input.charCodeAt(peg$currPos) === 125) { - s3 = peg$c9; - peg$currPos++; - } else { - s3 = peg$FAILED; - if (peg$silentFails === 0) { - peg$fail(peg$c10); - } - } - if (s3 !== peg$FAILED) { - peg$savedPos = s0; - s1 = peg$c12(s2); - s0 = s1; - } else { - peg$currPos = s0; - s0 = peg$FAILED; - } - } else { - peg$currPos = s0; - s0 = peg$FAILED; - } - } else { - peg$currPos = s0; - s0 = peg$FAILED; - } - } - - return s0; - } - - function peg$parseunbound_segments() { - let s0, s1, s2, s3; - - s0 = peg$currPos; - s1 = peg$parseunbound_terminal(); - if (s1 !== peg$FAILED) { - if (input.charCodeAt(peg$currPos) === 47) { - s2 = peg$c0; - peg$currPos++; - } else { - s2 = peg$FAILED; - if (peg$silentFails === 0) { - peg$fail(peg$c1); - } - } - if (s2 !== peg$FAILED) { - s3 = peg$parseunbound_segments(); - if (s3 !== peg$FAILED) { - peg$savedPos = s0; - s1 = peg$c13(s1, s3); - s0 = s1; - } else { - peg$currPos = s0; - s0 = peg$FAILED; - } - } else { - peg$currPos = s0; - s0 = peg$FAILED; - } - } else { - peg$currPos = s0; - s0 = peg$FAILED; - } - if (s0 === peg$FAILED) { - s0 = peg$parseunbound_terminal(); - } - - return s0; - } - - function peg$parsebound_terminal() { - let s0, s1; - - s0 = peg$currPos; - s1 = peg$parseunbound_terminal(); - if (s1 !== peg$FAILED) { - peg$savedPos = s0; - s1 = peg$c14(s1); - } - s0 = s1; - - return s0; - } - - function peg$parseunbound_terminal() { - let s0, s1; - - s0 = peg$currPos; - if (input.substr(peg$currPos, 2) === peg$c15) { - s1 = peg$c15; - peg$currPos += 2; - } else { - s1 = peg$FAILED; - if (peg$silentFails === 0) { - peg$fail(peg$c16); - } - } - if (s1 === peg$FAILED) { - if (input.charCodeAt(peg$currPos) === 42) { - s1 = peg$c17; - peg$currPos++; - } else { - s1 = peg$FAILED; - if (peg$silentFails === 0) { - peg$fail(peg$c18); - } - } - if (s1 === peg$FAILED) { - s1 = peg$parseliteral(); - } - } - if (s1 !== peg$FAILED) { - peg$savedPos = s0; - s1 = peg$c19(s1); - } - s0 = s1; - - return s0; - } - - function peg$parseliteral() { - let s0, s1, s2; - - s0 = peg$currPos; - s1 = []; - if (peg$c20.test(input.charAt(peg$currPos))) { - s2 = input.charAt(peg$currPos); - peg$currPos++; - } else { - s2 = peg$FAILED; - if (peg$silentFails === 0) { - peg$fail(peg$c21); - } - } - if (s2 !== peg$FAILED) { - while (s2 !== peg$FAILED) { - s1.push(s2); - if (peg$c20.test(input.charAt(peg$currPos))) { - s2 = input.charAt(peg$currPos); - peg$currPos++; - } else { - s2 = peg$FAILED; - if (peg$silentFails === 0) { - peg$fail(peg$c21); - } - } - } - } else { - s1 = peg$FAILED; - } - if (s1 !== peg$FAILED) { - peg$savedPos = s0; - s1 = peg$c22(s1); - } - s0 = s1; - - return s0; - } - - const extras = require("./parserExtras"); - - peg$result = peg$startRuleFunction(); - - if (peg$result !== peg$FAILED && peg$currPos === input.length) { - return peg$result; - } else { - if (peg$result !== peg$FAILED && peg$currPos < input.length) { - peg$fail({ type: "end", description: "end of input" }); - } - - throw peg$buildException( - null, - peg$maxFailExpected, - peg$maxFailPos < input.length ? input.charAt(peg$maxFailPos) : null, - peg$maxFailPos < input.length - ? peg$computeLocation(peg$maxFailPos, peg$maxFailPos + 1) - : peg$computeLocation(peg$maxFailPos, peg$maxFailPos) - ); - } - } - - return { - SyntaxError: peg$SyntaxError, - parse: peg$parse - }; -})(); diff --git a/src/pathTemplateParser.pegjs b/src/pathTemplateParser.pegjs deleted file mode 100644 index 7462b8559..000000000 --- a/src/pathTemplateParser.pegjs +++ /dev/null @@ -1,86 +0,0 @@ -/** - * Copyright 2020 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -{ - const extras = require('./parser_extras'); -} - - -template - = '/' segments:bound_segments { return segments; } - / segments:bound_segments { return segments; } - - -bound_segments - = s:bound_segment '/' segments:bound_segments { - return s.concat(segments); - } - / bound_segment - - -bound_segment - = s:( bound_terminal / variable ) { return s; } - - -variable - = '{' l:literal '=' segments:unbound_segments '}' { - return ([ - { kind: extras.BINDING, literal: l }, - segments, - { kind: extras.END_BINDING, literal: '' } - ]).reduce((a, b) => a.concat(b), []); - } - / '{' l:literal '}' { - return [ - { kind: extras.BINDING, literal: l }, - { kind: extras.TERMINAL, literal: '*' }, - { kind: extras.END_BINDING, literal: '' } - ]; - } - - -unbound_segments - = t:unbound_terminal '/' segments:unbound_segments { - return t.concat(segments); - } - / unbound_terminal - - -bound_terminal - = t:unbound_terminal { - if (t[0].literal === '*' || t[0].literal === '**') { - return [ - { - kind: extras.BINDING, - }, - t[0], - { kind: extras.END_BINDING, literal: '' } - ]; - } else { - return t; - } - } - - -unbound_terminal - = l:( '**' / '*' / literal ) { - return [ - { kind: extras.TERMINAL, literal: l } - ]; - } - -literal - = cs:[^*=}{/]+ { return cs.join('') } diff --git a/test/unit/pathTemplate.ts b/test/unit/pathTemplate.ts index 81ebb5e0a..36b60d13a 100644 --- a/test/unit/pathTemplate.ts +++ b/test/unit/pathTemplate.ts @@ -22,8 +22,8 @@ describe('PathTemplate', () => { describe('constructor', () => { it('should parse and obtain the correct number of segments', () => { const t = new PathTemplate('a/b/**/*/{a=hello/world}'); - assert.strictEqual(t.segments.length, 12); - assert.strictEqual(t.size, 6); + assert.strictEqual(t.segments.length, 5); + assert.strictEqual(t.size, 5); }); it('should fail on multiple path wildcards', () => { diff --git a/test/unit/pathTemplateParser.ts b/test/unit/pathTemplateParser.ts deleted file mode 100644 index 111235c75..000000000 --- a/test/unit/pathTemplateParser.ts +++ /dev/null @@ -1,59 +0,0 @@ -/** - * Copyright 2020 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import * as assert from 'assert'; -import {describe, it} from 'mocha'; - -describe('The PathTemplate parser', () => { - it('should load the pegjs generated module ok', () => { - // eslint-disable-next-line @typescript-eslint/no-var-requires - const parser = require('../../src/pathTemplateParser'); - assert.notStrictEqual(parser, null); - }); - - describe('function `parse`', () => { - // eslint-disable-next-line @typescript-eslint/no-var-requires - const parser = require('../../src/pathTemplateParser'); - - it('should succeed with valid inputs', () => { - const shouldPass = () => { - parser.parse('a/b/**/*/{a=hello/world}'); - }; - assert.doesNotThrow(shouldPass); - }); - - it('should fail on invalid tokens', () => { - const shouldFail = () => { - parser.parse('hello/wor* ld}'); - }; - assert.throws(shouldFail); - }); - - it('should fail on unexpected eof', () => { - const shouldFail = () => { - parser.parse('a/{hello=world'); - }; - assert.throws(shouldFail); - }); - - it('should fail on inner binding', () => { - const shouldFail = () => { - parser.parse('buckets/{hello={world}}'); - }; - assert.throws(shouldFail); - }); - }); -});