From 4498f0fd9972de1133d903b84659c7fbb3f96a06 Mon Sep 17 00:00:00 2001 From: Vladimir Gorej Date: Sat, 14 Dec 2024 22:27:44 +0100 Subject: [PATCH 1/8] feat: align ABNF grammar more with RFC 3986 BREAKING CHANGE: drop support for query and fragment being part of path Refs #145 --- README.md | 63 +++--- src/parse/callbacks/fragment-marker.js | 12 -- src/parse/callbacks/fragment.js | 12 -- src/parse/callbacks/path.js | 12 -- src/parse/callbacks/query-marker.js | 12 -- src/parse/callbacks/query.js | 12 -- src/parse/index.js | 10 - src/path-templating.bnf | 18 +- src/path-templating.js | 266 ++++++++++--------------- src/resolve.js | 8 +- test/parse.js | 52 +---- test/test.js | 2 +- 12 files changed, 142 insertions(+), 337 deletions(-) delete mode 100644 src/parse/callbacks/fragment-marker.js delete mode 100644 src/parse/callbacks/fragment.js delete mode 100644 src/parse/callbacks/path.js delete mode 100644 src/parse/callbacks/query-marker.js delete mode 100644 src/parse/callbacks/query.js diff --git a/README.md b/README.md index d573b74..c822674 100644 --- a/README.md +++ b/README.md @@ -89,17 +89,12 @@ parseResult.result.success; // => true length: 13, matched: 13, maxMatched: 13, - maxTreeDepth: 18, - nodeHits: 324 + maxTreeDepth: 20, + nodeHits: 232 }, ast: fnast { callbacks: [ 'path-template': [Function: pathTemplate], - path: [Function: path], - query: [Function: query], - 'query-marker': [Function: queryMarker], - fragment: [Function: fragment], - 'fragment-marker': [Function: fragmentMarker], slash: [Function: slash], 'path-literal': [Function: pathLiteral], 'template-expression': [Function: templateExpression], @@ -134,7 +129,6 @@ After running the above code, **parts** variable has the following shape: ```js [ [ 'path-template', '/pets/{petId}' ], - [ 'path', '/pets/{petId}' ], [ 'slash', '/' ], [ 'path-literal', 'pets' ], [ 'slash', '/' ], @@ -156,29 +150,26 @@ After running the above code, **xml** variable has the following content: ```xml - + /pets/{petId} /pets/{petId} - - /pets/{petId} - - / - - - pets - - - / - - - {petId} - - petId - - - + + / + + + pets + + + / + + + {petId} + + petId + + ``` @@ -248,21 +239,17 @@ The Path Templating is defined by the following [ABNF](https://tools.ietf.org/ht ```abnf ; OpenAPI Path Templating ABNF syntax -path-template = path [ query-marker query ] [ fragment-marker fragment ] -path = slash *( path-segment slash ) [ path-segment ] -path-segment = 1*( path-literal / template-expression ) -query = *( query-literal ) -query-literal = 1*( unreserved / pct-encoded / sub-delims / ":" / "@" / "/" / "?" / "&" / "=" ) -query-marker = "?" -fragment = *( fragment-literal ) -fragment-literal = 1*( unreserved / pct-encoded / sub-delims / ":" / "@" / "/" / "?" ) -fragment-marker = "#" +; Aligned with RFC 3986 (https://datatracker.ietf.org/doc/html/rfc3986#section-3.3) +path-template = slash [ path-template-nz ] +path-template-nz = path-segment *( slash path-segment ) slash = "/" -path-literal = 1*( unreserved / pct-encoded / sub-delims / ":" / "@" ) +path-segment = 1*( path-literal / template-expression ) +path-literal = 1*pchar template-expression = "{" template-expression-param-name "}" -template-expression-param-name = 1*( unreserved / pct-encoded / sub-delims / ":" / "@" ) +template-expression-param-name = 1*pchar ; Characters definitions (from RFC 3986) +pchar = unreserved / pct-encoded / sub-delims / ":" / "@" unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~" pct-encoded = "%" HEXDIG HEXDIG sub-delims = "!" / "$" / "&" / "'" / "(" / ")" diff --git a/src/parse/callbacks/fragment-marker.js b/src/parse/callbacks/fragment-marker.js deleted file mode 100644 index 5a17d66..0000000 --- a/src/parse/callbacks/fragment-marker.js +++ /dev/null @@ -1,12 +0,0 @@ -import { identifiers, utilities } from 'apg-lite'; - -const fragmentMarker = (state, chars, phraseIndex, phraseLength, data) => { - if (state === identifiers.SEM_PRE) { - data.push(['fragment-marker', utilities.charsToString(chars, phraseIndex, phraseLength)]); - } else if (state === identifiers.SEM_POST) { - /* not used in this example */ - } - return identifiers.SEM_OK; -}; - -export default fragmentMarker; diff --git a/src/parse/callbacks/fragment.js b/src/parse/callbacks/fragment.js deleted file mode 100644 index d5e54b8..0000000 --- a/src/parse/callbacks/fragment.js +++ /dev/null @@ -1,12 +0,0 @@ -import { identifiers, utilities } from 'apg-lite'; - -const fragment = (state, chars, phraseIndex, phraseLength, data) => { - if (state === identifiers.SEM_PRE) { - data.push(['fragment', utilities.charsToString(chars, phraseIndex, phraseLength)]); - } else if (state === identifiers.SEM_POST) { - /* not used in this example */ - } - return identifiers.SEM_OK; -}; - -export default fragment; diff --git a/src/parse/callbacks/path.js b/src/parse/callbacks/path.js deleted file mode 100644 index e8054fc..0000000 --- a/src/parse/callbacks/path.js +++ /dev/null @@ -1,12 +0,0 @@ -import { identifiers, utilities } from 'apg-lite'; - -const path = (state, chars, phraseIndex, phraseLength, data) => { - if (state === identifiers.SEM_PRE) { - data.push(['path', utilities.charsToString(chars, phraseIndex, phraseLength)]); - } else if (state === identifiers.SEM_POST) { - /* not used in this example */ - } - return identifiers.SEM_OK; -}; - -export default path; diff --git a/src/parse/callbacks/query-marker.js b/src/parse/callbacks/query-marker.js deleted file mode 100644 index 838e974..0000000 --- a/src/parse/callbacks/query-marker.js +++ /dev/null @@ -1,12 +0,0 @@ -import { identifiers, utilities } from 'apg-lite'; - -const queryMarker = (state, chars, phraseIndex, phraseLength, data) => { - if (state === identifiers.SEM_PRE) { - data.push(['query-marker', utilities.charsToString(chars, phraseIndex, phraseLength)]); - } else if (state === identifiers.SEM_POST) { - /* not used in this example */ - } - return identifiers.SEM_OK; -}; - -export default queryMarker; diff --git a/src/parse/callbacks/query.js b/src/parse/callbacks/query.js deleted file mode 100644 index 6201a11..0000000 --- a/src/parse/callbacks/query.js +++ /dev/null @@ -1,12 +0,0 @@ -import { identifiers, utilities } from 'apg-lite'; - -const query = (state, chars, phraseIndex, phraseLength, data) => { - if (state === identifiers.SEM_PRE) { - data.push(['query', utilities.charsToString(chars, phraseIndex, phraseLength)]); - } else if (state === identifiers.SEM_POST) { - /* not used in this example */ - } - return identifiers.SEM_OK; -}; - -export default query; diff --git a/src/parse/index.js b/src/parse/index.js index bb856fc..6389cfe 100644 --- a/src/parse/index.js +++ b/src/parse/index.js @@ -3,12 +3,7 @@ import { Ast as AST, Parser } from 'apg-lite'; import Grammar from '../path-templating.js'; import slashCallback from './callbacks/slash.js'; import pathTemplateCallback from './callbacks/path-template.js'; -import pathCallback from './callbacks/path.js'; import pathLiteralCallback from './callbacks/path-literal.js'; -import queryCallback from './callbacks/query.js'; -import queryMarkerCallback from './callbacks/query-marker.js'; -import fragmentCallback from './callbacks/fragment.js'; -import fragmentMarkerCallback from './callbacks/fragment-marker.js'; import templateExpressionCallback from './callbacks/template-expression.js'; import templateExpressionParamNameCallback from './callbacks/template-expression-param-name.js'; @@ -19,11 +14,6 @@ const parse = (pathTemplate) => { parser.ast = new AST(); parser.ast.callbacks['path-template'] = pathTemplateCallback; - parser.ast.callbacks['path'] = pathCallback; - parser.ast.callbacks['query'] = queryCallback; - parser.ast.callbacks['query-marker'] = queryMarkerCallback; - parser.ast.callbacks['fragment'] = fragmentCallback; - parser.ast.callbacks['fragment-marker'] = fragmentMarkerCallback; parser.ast.callbacks['slash'] = slashCallback; parser.ast.callbacks['path-literal'] = pathLiteralCallback; parser.ast.callbacks['template-expression'] = templateExpressionCallback; diff --git a/src/path-templating.bnf b/src/path-templating.bnf index 05b41d8..d97e62e 100644 --- a/src/path-templating.bnf +++ b/src/path-templating.bnf @@ -1,19 +1,15 @@ ; OpenAPI Path Templating ABNF syntax -path-template = path [ query-marker query ] [ fragment-marker fragment ] -path = slash *( path-segment slash ) [ path-segment ] -path-segment = 1*( path-literal / template-expression ) -query = *( query-literal ) -query-literal = 1*( unreserved / pct-encoded / sub-delims / ":" / "@" / "/" / "?" / "&" / "=" ) -query-marker = "?" -fragment = *( fragment-literal ) -fragment-literal = 1*( unreserved / pct-encoded / sub-delims / ":" / "@" / "/" / "?" ) -fragment-marker = "#" +; Aligned with RFC 3986 (https://datatracker.ietf.org/doc/html/rfc3986#section-3.3) +path-template = slash [ path-template-nz ] +path-template-nz = path-segment *( slash path-segment ) slash = "/" -path-literal = 1*( unreserved / pct-encoded / sub-delims / ":" / "@" ) +path-segment = 1*( path-literal / template-expression ) +path-literal = 1*pchar template-expression = "{" template-expression-param-name "}" -template-expression-param-name = 1*( unreserved / pct-encoded / sub-delims / ":" / "@" ) +template-expression-param-name = 1*pchar ; Characters definitions (from RFC 3986) +pchar = unreserved / pct-encoded / sub-delims / ":" / "@" unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~" pct-encoded = "%" HEXDIG HEXDIG sub-delims = "!" / "$" / "&" / "'" / "(" / ")" diff --git a/src/path-templating.js b/src/path-templating.js index c90fb15..08818e6 100644 --- a/src/path-templating.js +++ b/src/path-templating.js @@ -5,15 +5,15 @@ export default function grammar(){ // ``` // SUMMARY - // rules = 19 + // rules = 14 // udts = 0 - // opcodes = 102 + // opcodes = 64 // --- ABNF original opcodes - // ALT = 9 - // CAT = 7 - // REP = 11 - // RNM = 31 - // TLS = 41 + // ALT = 6 + // CAT = 5 + // REP = 5 + // RNM = 18 + // TLS = 27 // TBS = 0 // TRG = 3 // --- SABNF superset opcodes @@ -28,24 +28,19 @@ export default function grammar(){ /* RULES */ this.rules = []; this.rules[0] = { name: 'path-template', lower: 'path-template', index: 0, isBkr: false }; - this.rules[1] = { name: 'path', lower: 'path', index: 1, isBkr: false }; - this.rules[2] = { name: 'path-segment', lower: 'path-segment', index: 2, isBkr: false }; - this.rules[3] = { name: 'query', lower: 'query', index: 3, isBkr: false }; - this.rules[4] = { name: 'query-literal', lower: 'query-literal', index: 4, isBkr: false }; - this.rules[5] = { name: 'query-marker', lower: 'query-marker', index: 5, isBkr: false }; - this.rules[6] = { name: 'fragment', lower: 'fragment', index: 6, isBkr: false }; - this.rules[7] = { name: 'fragment-literal', lower: 'fragment-literal', index: 7, isBkr: false }; - this.rules[8] = { name: 'fragment-marker', lower: 'fragment-marker', index: 8, isBkr: false }; - this.rules[9] = { name: 'slash', lower: 'slash', index: 9, isBkr: false }; - this.rules[10] = { name: 'path-literal', lower: 'path-literal', index: 10, isBkr: false }; - this.rules[11] = { name: 'template-expression', lower: 'template-expression', index: 11, isBkr: false }; - this.rules[12] = { name: 'template-expression-param-name', lower: 'template-expression-param-name', index: 12, isBkr: false }; - this.rules[13] = { name: 'unreserved', lower: 'unreserved', index: 13, isBkr: false }; - this.rules[14] = { name: 'pct-encoded', lower: 'pct-encoded', index: 14, isBkr: false }; - this.rules[15] = { name: 'sub-delims', lower: 'sub-delims', index: 15, isBkr: false }; - this.rules[16] = { name: 'ALPHA', lower: 'alpha', index: 16, isBkr: false }; - this.rules[17] = { name: 'DIGIT', lower: 'digit', index: 17, isBkr: false }; - this.rules[18] = { name: 'HEXDIG', lower: 'hexdig', index: 18, isBkr: false }; + this.rules[1] = { name: 'path-template-nz', lower: 'path-template-nz', index: 1, isBkr: false }; + this.rules[2] = { name: 'slash', lower: 'slash', index: 2, isBkr: false }; + this.rules[3] = { name: 'path-segment', lower: 'path-segment', index: 3, isBkr: false }; + this.rules[4] = { name: 'path-literal', lower: 'path-literal', index: 4, isBkr: false }; + this.rules[5] = { name: 'template-expression', lower: 'template-expression', index: 5, isBkr: false }; + this.rules[6] = { name: 'template-expression-param-name', lower: 'template-expression-param-name', index: 6, isBkr: false }; + this.rules[7] = { name: 'pchar', lower: 'pchar', index: 7, isBkr: false }; + this.rules[8] = { name: 'unreserved', lower: 'unreserved', index: 8, isBkr: false }; + this.rules[9] = { name: 'pct-encoded', lower: 'pct-encoded', index: 9, isBkr: false }; + this.rules[10] = { name: 'sub-delims', lower: 'sub-delims', index: 10, isBkr: false }; + this.rules[11] = { name: 'ALPHA', lower: 'alpha', index: 11, isBkr: false }; + this.rules[12] = { name: 'DIGIT', lower: 'digit', index: 12, isBkr: false }; + this.rules[13] = { name: 'HEXDIG', lower: 'hexdig', index: 13, isBkr: false }; /* UDTS */ this.udts = []; @@ -53,182 +48,125 @@ export default function grammar(){ /* OPCODES */ /* path-template */ this.rules[0].opcodes = []; - this.rules[0].opcodes[0] = { type: 2, children: [1,2,6] };// CAT - this.rules[0].opcodes[1] = { type: 4, index: 1 };// RNM(path) + this.rules[0].opcodes[0] = { type: 2, children: [1,2] };// CAT + this.rules[0].opcodes[1] = { type: 4, index: 2 };// RNM(slash) this.rules[0].opcodes[2] = { type: 3, min: 0, max: 1 };// REP - this.rules[0].opcodes[3] = { type: 2, children: [4,5] };// CAT - this.rules[0].opcodes[4] = { type: 4, index: 5 };// RNM(query-marker) - this.rules[0].opcodes[5] = { type: 4, index: 3 };// RNM(query) - this.rules[0].opcodes[6] = { type: 3, min: 0, max: 1 };// REP - this.rules[0].opcodes[7] = { type: 2, children: [8,9] };// CAT - this.rules[0].opcodes[8] = { type: 4, index: 8 };// RNM(fragment-marker) - this.rules[0].opcodes[9] = { type: 4, index: 6 };// RNM(fragment) - - /* path */ + this.rules[0].opcodes[3] = { type: 4, index: 1 };// RNM(path-template-nz) + + /* path-template-nz */ this.rules[1].opcodes = []; - this.rules[1].opcodes[0] = { type: 2, children: [1,2,6] };// CAT - this.rules[1].opcodes[1] = { type: 4, index: 9 };// RNM(slash) + this.rules[1].opcodes[0] = { type: 2, children: [1,2] };// CAT + this.rules[1].opcodes[1] = { type: 4, index: 3 };// RNM(path-segment) this.rules[1].opcodes[2] = { type: 3, min: 0, max: Infinity };// REP this.rules[1].opcodes[3] = { type: 2, children: [4,5] };// CAT - this.rules[1].opcodes[4] = { type: 4, index: 2 };// RNM(path-segment) - this.rules[1].opcodes[5] = { type: 4, index: 9 };// RNM(slash) - this.rules[1].opcodes[6] = { type: 3, min: 0, max: 1 };// REP - this.rules[1].opcodes[7] = { type: 4, index: 2 };// RNM(path-segment) + this.rules[1].opcodes[4] = { type: 4, index: 2 };// RNM(slash) + this.rules[1].opcodes[5] = { type: 4, index: 3 };// RNM(path-segment) - /* path-segment */ + /* slash */ this.rules[2].opcodes = []; - this.rules[2].opcodes[0] = { type: 3, min: 1, max: Infinity };// REP - this.rules[2].opcodes[1] = { type: 1, children: [2,3] };// ALT - this.rules[2].opcodes[2] = { type: 4, index: 10 };// RNM(path-literal) - this.rules[2].opcodes[3] = { type: 4, index: 11 };// RNM(template-expression) + this.rules[2].opcodes[0] = { type: 7, string: [47] };// TLS - /* query */ + /* path-segment */ this.rules[3].opcodes = []; - this.rules[3].opcodes[0] = { type: 3, min: 0, max: Infinity };// REP - this.rules[3].opcodes[1] = { type: 4, index: 4 };// RNM(query-literal) + this.rules[3].opcodes[0] = { type: 3, min: 1, max: Infinity };// REP + this.rules[3].opcodes[1] = { type: 1, children: [2,3] };// ALT + this.rules[3].opcodes[2] = { type: 4, index: 4 };// RNM(path-literal) + this.rules[3].opcodes[3] = { type: 4, index: 5 };// RNM(template-expression) - /* query-literal */ + /* path-literal */ this.rules[4].opcodes = []; this.rules[4].opcodes[0] = { type: 3, min: 1, max: Infinity };// REP - this.rules[4].opcodes[1] = { type: 1, children: [2,3,4,5,6,7,8,9,10] };// ALT - this.rules[4].opcodes[2] = { type: 4, index: 13 };// RNM(unreserved) - this.rules[4].opcodes[3] = { type: 4, index: 14 };// RNM(pct-encoded) - this.rules[4].opcodes[4] = { type: 4, index: 15 };// RNM(sub-delims) - this.rules[4].opcodes[5] = { type: 7, string: [58] };// TLS - this.rules[4].opcodes[6] = { type: 7, string: [64] };// TLS - this.rules[4].opcodes[7] = { type: 7, string: [47] };// TLS - this.rules[4].opcodes[8] = { type: 7, string: [63] };// TLS - this.rules[4].opcodes[9] = { type: 7, string: [38] };// TLS - this.rules[4].opcodes[10] = { type: 7, string: [61] };// TLS - - /* query-marker */ + this.rules[4].opcodes[1] = { type: 4, index: 7 };// RNM(pchar) + + /* template-expression */ this.rules[5].opcodes = []; - this.rules[5].opcodes[0] = { type: 7, string: [63] };// TLS + this.rules[5].opcodes[0] = { type: 2, children: [1,2,3] };// CAT + this.rules[5].opcodes[1] = { type: 7, string: [123] };// TLS + this.rules[5].opcodes[2] = { type: 4, index: 6 };// RNM(template-expression-param-name) + this.rules[5].opcodes[3] = { type: 7, string: [125] };// TLS - /* fragment */ + /* template-expression-param-name */ this.rules[6].opcodes = []; - this.rules[6].opcodes[0] = { type: 3, min: 0, max: Infinity };// REP - this.rules[6].opcodes[1] = { type: 4, index: 7 };// RNM(fragment-literal) + this.rules[6].opcodes[0] = { type: 3, min: 1, max: Infinity };// REP + this.rules[6].opcodes[1] = { type: 4, index: 7 };// RNM(pchar) - /* fragment-literal */ + /* pchar */ this.rules[7].opcodes = []; - this.rules[7].opcodes[0] = { type: 3, min: 1, max: Infinity };// REP - this.rules[7].opcodes[1] = { type: 1, children: [2,3,4,5,6,7,8] };// ALT - this.rules[7].opcodes[2] = { type: 4, index: 13 };// RNM(unreserved) - this.rules[7].opcodes[3] = { type: 4, index: 14 };// RNM(pct-encoded) - this.rules[7].opcodes[4] = { type: 4, index: 15 };// RNM(sub-delims) - this.rules[7].opcodes[5] = { type: 7, string: [58] };// TLS - this.rules[7].opcodes[6] = { type: 7, string: [64] };// TLS - this.rules[7].opcodes[7] = { type: 7, string: [47] };// TLS - this.rules[7].opcodes[8] = { type: 7, string: [63] };// TLS - - /* fragment-marker */ - this.rules[8].opcodes = []; - this.rules[8].opcodes[0] = { type: 7, string: [35] };// TLS - - /* slash */ - this.rules[9].opcodes = []; - this.rules[9].opcodes[0] = { type: 7, string: [47] };// TLS - - /* path-literal */ - this.rules[10].opcodes = []; - this.rules[10].opcodes[0] = { type: 3, min: 1, max: Infinity };// REP - this.rules[10].opcodes[1] = { type: 1, children: [2,3,4,5,6] };// ALT - this.rules[10].opcodes[2] = { type: 4, index: 13 };// RNM(unreserved) - this.rules[10].opcodes[3] = { type: 4, index: 14 };// RNM(pct-encoded) - this.rules[10].opcodes[4] = { type: 4, index: 15 };// RNM(sub-delims) - this.rules[10].opcodes[5] = { type: 7, string: [58] };// TLS - this.rules[10].opcodes[6] = { type: 7, string: [64] };// TLS - - /* template-expression */ - this.rules[11].opcodes = []; - this.rules[11].opcodes[0] = { type: 2, children: [1,2,3] };// CAT - this.rules[11].opcodes[1] = { type: 7, string: [123] };// TLS - this.rules[11].opcodes[2] = { type: 4, index: 12 };// RNM(template-expression-param-name) - this.rules[11].opcodes[3] = { type: 7, string: [125] };// TLS - - /* template-expression-param-name */ - this.rules[12].opcodes = []; - this.rules[12].opcodes[0] = { type: 3, min: 1, max: Infinity };// REP - this.rules[12].opcodes[1] = { type: 1, children: [2,3,4,5,6] };// ALT - this.rules[12].opcodes[2] = { type: 4, index: 13 };// RNM(unreserved) - this.rules[12].opcodes[3] = { type: 4, index: 14 };// RNM(pct-encoded) - this.rules[12].opcodes[4] = { type: 4, index: 15 };// RNM(sub-delims) - this.rules[12].opcodes[5] = { type: 7, string: [58] };// TLS - this.rules[12].opcodes[6] = { type: 7, string: [64] };// TLS + this.rules[7].opcodes[0] = { type: 1, children: [1,2,3,4,5] };// ALT + this.rules[7].opcodes[1] = { type: 4, index: 8 };// RNM(unreserved) + this.rules[7].opcodes[2] = { type: 4, index: 9 };// RNM(pct-encoded) + this.rules[7].opcodes[3] = { type: 4, index: 10 };// RNM(sub-delims) + this.rules[7].opcodes[4] = { type: 7, string: [58] };// TLS + this.rules[7].opcodes[5] = { type: 7, string: [64] };// TLS /* unreserved */ - this.rules[13].opcodes = []; - this.rules[13].opcodes[0] = { type: 1, children: [1,2,3,4,5,6] };// ALT - this.rules[13].opcodes[1] = { type: 4, index: 16 };// RNM(ALPHA) - this.rules[13].opcodes[2] = { type: 4, index: 17 };// RNM(DIGIT) - this.rules[13].opcodes[3] = { type: 7, string: [45] };// TLS - this.rules[13].opcodes[4] = { type: 7, string: [46] };// TLS - this.rules[13].opcodes[5] = { type: 7, string: [95] };// TLS - this.rules[13].opcodes[6] = { type: 7, string: [126] };// TLS + this.rules[8].opcodes = []; + this.rules[8].opcodes[0] = { type: 1, children: [1,2,3,4,5,6] };// ALT + this.rules[8].opcodes[1] = { type: 4, index: 11 };// RNM(ALPHA) + this.rules[8].opcodes[2] = { type: 4, index: 12 };// RNM(DIGIT) + this.rules[8].opcodes[3] = { type: 7, string: [45] };// TLS + this.rules[8].opcodes[4] = { type: 7, string: [46] };// TLS + this.rules[8].opcodes[5] = { type: 7, string: [95] };// TLS + this.rules[8].opcodes[6] = { type: 7, string: [126] };// TLS /* pct-encoded */ - this.rules[14].opcodes = []; - this.rules[14].opcodes[0] = { type: 2, children: [1,2,3] };// CAT - this.rules[14].opcodes[1] = { type: 7, string: [37] };// TLS - this.rules[14].opcodes[2] = { type: 4, index: 18 };// RNM(HEXDIG) - this.rules[14].opcodes[3] = { type: 4, index: 18 };// RNM(HEXDIG) + this.rules[9].opcodes = []; + this.rules[9].opcodes[0] = { type: 2, children: [1,2,3] };// CAT + this.rules[9].opcodes[1] = { type: 7, string: [37] };// TLS + this.rules[9].opcodes[2] = { type: 4, index: 13 };// RNM(HEXDIG) + this.rules[9].opcodes[3] = { type: 4, index: 13 };// RNM(HEXDIG) /* sub-delims */ - this.rules[15].opcodes = []; - this.rules[15].opcodes[0] = { type: 1, children: [1,2,3,4,5,6,7,8,9,10,11] };// ALT - this.rules[15].opcodes[1] = { type: 7, string: [33] };// TLS - this.rules[15].opcodes[2] = { type: 7, string: [36] };// TLS - this.rules[15].opcodes[3] = { type: 7, string: [38] };// TLS - this.rules[15].opcodes[4] = { type: 7, string: [39] };// TLS - this.rules[15].opcodes[5] = { type: 7, string: [40] };// TLS - this.rules[15].opcodes[6] = { type: 7, string: [41] };// TLS - this.rules[15].opcodes[7] = { type: 7, string: [42] };// TLS - this.rules[15].opcodes[8] = { type: 7, string: [43] };// TLS - this.rules[15].opcodes[9] = { type: 7, string: [44] };// TLS - this.rules[15].opcodes[10] = { type: 7, string: [59] };// TLS - this.rules[15].opcodes[11] = { type: 7, string: [61] };// TLS + this.rules[10].opcodes = []; + this.rules[10].opcodes[0] = { type: 1, children: [1,2,3,4,5,6,7,8,9,10,11] };// ALT + this.rules[10].opcodes[1] = { type: 7, string: [33] };// TLS + this.rules[10].opcodes[2] = { type: 7, string: [36] };// TLS + this.rules[10].opcodes[3] = { type: 7, string: [38] };// TLS + this.rules[10].opcodes[4] = { type: 7, string: [39] };// TLS + this.rules[10].opcodes[5] = { type: 7, string: [40] };// TLS + this.rules[10].opcodes[6] = { type: 7, string: [41] };// TLS + this.rules[10].opcodes[7] = { type: 7, string: [42] };// TLS + this.rules[10].opcodes[8] = { type: 7, string: [43] };// TLS + this.rules[10].opcodes[9] = { type: 7, string: [44] };// TLS + this.rules[10].opcodes[10] = { type: 7, string: [59] };// TLS + this.rules[10].opcodes[11] = { type: 7, string: [61] };// TLS /* ALPHA */ - this.rules[16].opcodes = []; - this.rules[16].opcodes[0] = { type: 1, children: [1,2] };// ALT - this.rules[16].opcodes[1] = { type: 5, min: 65, max: 90 };// TRG - this.rules[16].opcodes[2] = { type: 5, min: 97, max: 122 };// TRG + this.rules[11].opcodes = []; + this.rules[11].opcodes[0] = { type: 1, children: [1,2] };// ALT + this.rules[11].opcodes[1] = { type: 5, min: 65, max: 90 };// TRG + this.rules[11].opcodes[2] = { type: 5, min: 97, max: 122 };// TRG /* DIGIT */ - this.rules[17].opcodes = []; - this.rules[17].opcodes[0] = { type: 5, min: 48, max: 57 };// TRG + this.rules[12].opcodes = []; + this.rules[12].opcodes[0] = { type: 5, min: 48, max: 57 };// TRG /* HEXDIG */ - this.rules[18].opcodes = []; - this.rules[18].opcodes[0] = { type: 1, children: [1,2,3,4,5,6,7] };// ALT - this.rules[18].opcodes[1] = { type: 4, index: 17 };// RNM(DIGIT) - this.rules[18].opcodes[2] = { type: 7, string: [97] };// TLS - this.rules[18].opcodes[3] = { type: 7, string: [98] };// TLS - this.rules[18].opcodes[4] = { type: 7, string: [99] };// TLS - this.rules[18].opcodes[5] = { type: 7, string: [100] };// TLS - this.rules[18].opcodes[6] = { type: 7, string: [101] };// TLS - this.rules[18].opcodes[7] = { type: 7, string: [102] };// TLS + this.rules[13].opcodes = []; + this.rules[13].opcodes[0] = { type: 1, children: [1,2,3,4,5,6,7] };// ALT + this.rules[13].opcodes[1] = { type: 4, index: 12 };// RNM(DIGIT) + this.rules[13].opcodes[2] = { type: 7, string: [97] };// TLS + this.rules[13].opcodes[3] = { type: 7, string: [98] };// TLS + this.rules[13].opcodes[4] = { type: 7, string: [99] };// TLS + this.rules[13].opcodes[5] = { type: 7, string: [100] };// TLS + this.rules[13].opcodes[6] = { type: 7, string: [101] };// TLS + this.rules[13].opcodes[7] = { type: 7, string: [102] };// TLS // The `toString()` function will display the original grammar file(s) that produced these opcodes. this.toString = function toString(){ let str = ""; str += "; OpenAPI Path Templating ABNF syntax\n"; - str += "path-template = path [ query-marker query ] [ fragment-marker fragment ]\n"; - str += "path = slash *( path-segment slash ) [ path-segment ]\n"; - str += "path-segment = 1*( path-literal / template-expression )\n"; - str += "query = *( query-literal )\n"; - str += "query-literal = 1*( unreserved / pct-encoded / sub-delims / \":\" / \"@\" / \"/\" / \"?\" / \"&\" / \"=\" )\n"; - str += "query-marker = \"?\"\n"; - str += "fragment = *( fragment-literal )\n"; - str += "fragment-literal = 1*( unreserved / pct-encoded / sub-delims / \":\" / \"@\" / \"/\" / \"?\" )\n"; - str += "fragment-marker = \"#\"\n"; + str += "; Aligned with RFC 3986 (https://datatracker.ietf.org/doc/html/rfc3986#section-3.3)\n"; + str += "path-template = slash [ path-template-nz ]\n"; + str += "path-template-nz = path-segment *( slash path-segment )\n"; str += "slash = \"/\"\n"; - str += "path-literal = 1*( unreserved / pct-encoded / sub-delims / \":\" / \"@\" )\n"; + str += "path-segment = 1*( path-literal / template-expression )\n"; + str += "path-literal = 1*pchar\n"; str += "template-expression = \"{\" template-expression-param-name \"}\"\n"; - str += "template-expression-param-name = 1*( unreserved / pct-encoded / sub-delims / \":\" / \"@\" )\n"; + str += "template-expression-param-name = 1*pchar\n"; str += "\n"; str += "; Characters definitions (from RFC 3986)\n"; + str += "pchar = unreserved / pct-encoded / sub-delims / \":\" / \"@\"\n"; str += "unreserved = ALPHA / DIGIT / \"-\" / \".\" / \"_\" / \"~\"\n"; str += "pct-encoded = \"%\" HEXDIG HEXDIG\n"; str += "sub-delims = \"!\" / \"$\" / \"&\" / \"'\" / \"(\" / \")\"\n"; diff --git a/src/resolve.js b/src/resolve.js index c987cce..219d150 100644 --- a/src/resolve.js +++ b/src/resolve.js @@ -18,13 +18,7 @@ export const encodePathComponent = (parameterValue) => { return encodeURIComponent(parameterValue).replace(/%5B/g, '[').replace(/%5D/g, ']'); }; -const significantTypes = [ - 'slash', - 'path-literal', - 'query-marker', - 'query-literal', - 'template-expression-param-name', -]; +const significantTypes = ['slash', 'path-literal', 'template-expression-param-name']; const resolve = (pathTemplate, parameters, options = {}) => { const defaultOptions = { encoder: encodePathComponent }; diff --git a/test/parse.js b/test/parse.js index e79bcb2..514f5ce 100644 --- a/test/parse.js +++ b/test/parse.js @@ -14,7 +14,6 @@ describe('parse', function () { assert.isTrue(parseResult.result.success); assert.deepEqual(parts, [ ['path-template', '/pets/{petId}'], - ['path', '/pets/{petId}'], ['slash', '/'], ['path-literal', 'pets'], ['slash', '/'], @@ -34,7 +33,6 @@ describe('parse', function () { assert.isTrue(parseResult.result.success); assert.deepEqual(parts, [ ['path-template', '/{petId}'], - ['path', '/{petId}'], ['slash', '/'], ['template-expression', '{petId}'], ['template-expression-param-name', 'petId'], @@ -52,7 +50,6 @@ describe('parse', function () { assert.isTrue(parseResult.result.success); assert.deepEqual(parts, [ ['path-template', '/a{petId}'], - ['path', '/a{petId}'], ['slash', '/'], ['path-literal', 'a'], ['template-expression', '{petId}'], @@ -62,21 +59,10 @@ describe('parse', function () { }); context('/pets?offset=0&limit=10', function () { - specify('should parse and translate', function () { + specify('should not parse with query', function () { const parseResult = parse('/pets?offset=0&limit=10'); - const parts = []; - parseResult.ast.translate(parts); - - assert.isTrue(parseResult.result.success); - assert.deepEqual(parts, [ - ['path-template', '/pets?offset=0&limit=10'], - ['path', '/pets'], - ['slash', '/'], - ['path-literal', 'pets'], - ['query-marker', '?'], - ['query', 'offset=0&limit=10'], - ]); + assert.isFalse(parseResult.result.success); }); }); @@ -97,42 +83,18 @@ describe('parse', function () { }); context('/pets#fragment', function () { - specify('should parse', function () { + specify('should not parse with fragment', function () { const parseResult = parse('/pets#fragment'); - const parts = []; - parseResult.ast.translate(parts); - - assert.isTrue(parseResult.result.success); - assert.deepEqual(parts, [ - ['path-template', '/pets#fragment'], - ['path', '/pets'], - ['slash', '/'], - ['path-literal', 'pets'], - ['fragment-marker', '#'], - ['fragment', 'fragment'], - ]); + assert.isFalse(parseResult.result.success); }); }); context('/pets?offset=0#fragment', function () { - specify('should parse', function () { + specify('should not parse with query and fragment', function () { const parseResult = parse('/pets?offset=0#fragment'); - const parts = []; - parseResult.ast.translate(parts); - - assert.isTrue(parseResult.result.success); - assert.deepEqual(parts, [ - ['path-template', '/pets?offset=0#fragment'], - ['path', '/pets'], - ['slash', '/'], - ['path-literal', 'pets'], - ['query-marker', '?'], - ['query', 'offset=0'], - ['fragment-marker', '#'], - ['fragment', 'fragment'], - ]); + assert.isFalse(parseResult.result.success); }); }); @@ -146,7 +108,6 @@ describe('parse', function () { assert.isTrue(parseResult.result.success); assert.deepEqual(parts, [ ['path-template', '/pets/mine'], - ['path', '/pets/mine'], ['slash', '/'], ['path-literal', 'pets'], ['slash', '/'], @@ -165,7 +126,6 @@ describe('parse', function () { assert.isTrue(parseResult.result.success); assert.deepEqual(parts, [ ['path-template', '/'], - ['path', '/'], ['slash', '/'], ]); }); diff --git a/test/test.js b/test/test.js index e1b38d3..9194450 100644 --- a/test/test.js +++ b/test/test.js @@ -10,7 +10,6 @@ describe('test', function () { assert.isTrue(test('/books/{id}')); assert.isTrue(test('/a{test}')); assert.isTrue(test('/{entity}/{another-entity}/me')); - assert.isTrue(test('/pets?offset=0&limit=10')); assert.isTrue(test('/')); }); @@ -19,6 +18,7 @@ describe('test', function () { assert.isFalse(test('1')); assert.isFalse(test('{petId}')); assert.isFalse(test('/pet/{petId')); + assert.isFalse(test('/pets?offset=0&limit=10')); assert.isFalse(test('/pets?offset={offset}&limit={limit}')); assert.isFalse(test('/pets?offset{offset}limit={limit}')); assert.isFalse(test(1)); From 23d39acef72be4429ef5821cc763d869b0aa9eec Mon Sep 17 00:00:00 2001 From: Vladimir Gorej Date: Sun, 15 Dec 2024 11:13:48 +0100 Subject: [PATCH 2/8] docs(README): fix SABNF link --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index c822674..0149897 100644 --- a/README.md +++ b/README.md @@ -63,7 +63,7 @@ You can install `openapi-path-templating` using `npm`: ### Usage `openapi-path-templating` currently supports **parsing**, **validation** and **resolution**. -Both parser and validator are based on a superset of [ABNF](https://www.rfc-editor.org/rfc/rfc5234) ([SABNF](https://cs.github.com/ldthomas/apg-js2/blob/master/SABNF.md)) +Both parser and validator are based on a superset of [ABNF](https://www.rfc-editor.org/rfc/rfc5234) ([SABNF](https://github.com/ldthomas/apg-js2/blob/master/SABNF.md)) and use [apg-lite](https://github.com/ldthomas/apg-lite) parser generator. #### Parsing From b2fa10735cd583cc9b9b6912e06b75b891b0ea6d Mon Sep 17 00:00:00 2001 From: Vladimir Gorej Date: Mon, 16 Dec 2024 15:28:55 +0100 Subject: [PATCH 3/8] fix: return support for trailing slash --- src/path-templating.bnf | 6 +- src/path-templating.js | 182 +++++++++++++++++++--------------------- test/parse.js | 22 ++++- 3 files changed, 109 insertions(+), 101 deletions(-) diff --git a/src/path-templating.bnf b/src/path-templating.bnf index d97e62e..4c64eb8 100644 --- a/src/path-templating.bnf +++ b/src/path-templating.bnf @@ -1,9 +1,7 @@ ; OpenAPI Path Templating ABNF syntax -; Aligned with RFC 3986 (https://datatracker.ietf.org/doc/html/rfc3986#section-3.3) -path-template = slash [ path-template-nz ] -path-template-nz = path-segment *( slash path-segment ) -slash = "/" +path-template = slash *( path-segment slash ) [ path-segment ] path-segment = 1*( path-literal / template-expression ) +slash = "/" path-literal = 1*pchar template-expression = "{" template-expression-param-name "}" template-expression-param-name = 1*pchar diff --git a/src/path-templating.js b/src/path-templating.js index 08818e6..37dd74b 100644 --- a/src/path-templating.js +++ b/src/path-templating.js @@ -5,14 +5,14 @@ export default function grammar(){ // ``` // SUMMARY - // rules = 14 + // rules = 13 // udts = 0 - // opcodes = 64 + // opcodes = 62 // --- ABNF original opcodes // ALT = 6 - // CAT = 5 + // CAT = 4 // REP = 5 - // RNM = 18 + // RNM = 17 // TLS = 27 // TBS = 0 // TRG = 3 @@ -28,19 +28,18 @@ export default function grammar(){ /* RULES */ this.rules = []; this.rules[0] = { name: 'path-template', lower: 'path-template', index: 0, isBkr: false }; - this.rules[1] = { name: 'path-template-nz', lower: 'path-template-nz', index: 1, isBkr: false }; + this.rules[1] = { name: 'path-segment', lower: 'path-segment', index: 1, isBkr: false }; this.rules[2] = { name: 'slash', lower: 'slash', index: 2, isBkr: false }; - this.rules[3] = { name: 'path-segment', lower: 'path-segment', index: 3, isBkr: false }; - this.rules[4] = { name: 'path-literal', lower: 'path-literal', index: 4, isBkr: false }; - this.rules[5] = { name: 'template-expression', lower: 'template-expression', index: 5, isBkr: false }; - this.rules[6] = { name: 'template-expression-param-name', lower: 'template-expression-param-name', index: 6, isBkr: false }; - this.rules[7] = { name: 'pchar', lower: 'pchar', index: 7, isBkr: false }; - this.rules[8] = { name: 'unreserved', lower: 'unreserved', index: 8, isBkr: false }; - this.rules[9] = { name: 'pct-encoded', lower: 'pct-encoded', index: 9, isBkr: false }; - this.rules[10] = { name: 'sub-delims', lower: 'sub-delims', index: 10, isBkr: false }; - this.rules[11] = { name: 'ALPHA', lower: 'alpha', index: 11, isBkr: false }; - this.rules[12] = { name: 'DIGIT', lower: 'digit', index: 12, isBkr: false }; - this.rules[13] = { name: 'HEXDIG', lower: 'hexdig', index: 13, isBkr: false }; + this.rules[3] = { name: 'path-literal', lower: 'path-literal', index: 3, isBkr: false }; + this.rules[4] = { name: 'template-expression', lower: 'template-expression', index: 4, isBkr: false }; + this.rules[5] = { name: 'template-expression-param-name', lower: 'template-expression-param-name', index: 5, isBkr: false }; + this.rules[6] = { name: 'pchar', lower: 'pchar', index: 6, isBkr: false }; + this.rules[7] = { name: 'unreserved', lower: 'unreserved', index: 7, isBkr: false }; + this.rules[8] = { name: 'pct-encoded', lower: 'pct-encoded', index: 8, isBkr: false }; + this.rules[9] = { name: 'sub-delims', lower: 'sub-delims', index: 9, isBkr: false }; + this.rules[10] = { name: 'ALPHA', lower: 'alpha', index: 10, isBkr: false }; + this.rules[11] = { name: 'DIGIT', lower: 'digit', index: 11, isBkr: false }; + this.rules[12] = { name: 'HEXDIG', lower: 'hexdig', index: 12, isBkr: false }; /* UDTS */ this.udts = []; @@ -48,119 +47,112 @@ export default function grammar(){ /* OPCODES */ /* path-template */ this.rules[0].opcodes = []; - this.rules[0].opcodes[0] = { type: 2, children: [1,2] };// CAT + this.rules[0].opcodes[0] = { type: 2, children: [1,2,6] };// CAT this.rules[0].opcodes[1] = { type: 4, index: 2 };// RNM(slash) - this.rules[0].opcodes[2] = { type: 3, min: 0, max: 1 };// REP - this.rules[0].opcodes[3] = { type: 4, index: 1 };// RNM(path-template-nz) + this.rules[0].opcodes[2] = { type: 3, min: 0, max: Infinity };// REP + this.rules[0].opcodes[3] = { type: 2, children: [4,5] };// CAT + this.rules[0].opcodes[4] = { type: 4, index: 1 };// RNM(path-segment) + this.rules[0].opcodes[5] = { type: 4, index: 2 };// RNM(slash) + this.rules[0].opcodes[6] = { type: 3, min: 0, max: 1 };// REP + this.rules[0].opcodes[7] = { type: 4, index: 1 };// RNM(path-segment) - /* path-template-nz */ + /* path-segment */ this.rules[1].opcodes = []; - this.rules[1].opcodes[0] = { type: 2, children: [1,2] };// CAT - this.rules[1].opcodes[1] = { type: 4, index: 3 };// RNM(path-segment) - this.rules[1].opcodes[2] = { type: 3, min: 0, max: Infinity };// REP - this.rules[1].opcodes[3] = { type: 2, children: [4,5] };// CAT - this.rules[1].opcodes[4] = { type: 4, index: 2 };// RNM(slash) - this.rules[1].opcodes[5] = { type: 4, index: 3 };// RNM(path-segment) + this.rules[1].opcodes[0] = { type: 3, min: 1, max: Infinity };// REP + this.rules[1].opcodes[1] = { type: 1, children: [2,3] };// ALT + this.rules[1].opcodes[2] = { type: 4, index: 3 };// RNM(path-literal) + this.rules[1].opcodes[3] = { type: 4, index: 4 };// RNM(template-expression) /* slash */ this.rules[2].opcodes = []; this.rules[2].opcodes[0] = { type: 7, string: [47] };// TLS - /* path-segment */ + /* path-literal */ this.rules[3].opcodes = []; this.rules[3].opcodes[0] = { type: 3, min: 1, max: Infinity };// REP - this.rules[3].opcodes[1] = { type: 1, children: [2,3] };// ALT - this.rules[3].opcodes[2] = { type: 4, index: 4 };// RNM(path-literal) - this.rules[3].opcodes[3] = { type: 4, index: 5 };// RNM(template-expression) - - /* path-literal */ - this.rules[4].opcodes = []; - this.rules[4].opcodes[0] = { type: 3, min: 1, max: Infinity };// REP - this.rules[4].opcodes[1] = { type: 4, index: 7 };// RNM(pchar) + this.rules[3].opcodes[1] = { type: 4, index: 6 };// RNM(pchar) /* template-expression */ - this.rules[5].opcodes = []; - this.rules[5].opcodes[0] = { type: 2, children: [1,2,3] };// CAT - this.rules[5].opcodes[1] = { type: 7, string: [123] };// TLS - this.rules[5].opcodes[2] = { type: 4, index: 6 };// RNM(template-expression-param-name) - this.rules[5].opcodes[3] = { type: 7, string: [125] };// TLS + this.rules[4].opcodes = []; + this.rules[4].opcodes[0] = { type: 2, children: [1,2,3] };// CAT + this.rules[4].opcodes[1] = { type: 7, string: [123] };// TLS + this.rules[4].opcodes[2] = { type: 4, index: 5 };// RNM(template-expression-param-name) + this.rules[4].opcodes[3] = { type: 7, string: [125] };// TLS /* template-expression-param-name */ - this.rules[6].opcodes = []; - this.rules[6].opcodes[0] = { type: 3, min: 1, max: Infinity };// REP - this.rules[6].opcodes[1] = { type: 4, index: 7 };// RNM(pchar) + this.rules[5].opcodes = []; + this.rules[5].opcodes[0] = { type: 3, min: 1, max: Infinity };// REP + this.rules[5].opcodes[1] = { type: 4, index: 6 };// RNM(pchar) /* pchar */ - this.rules[7].opcodes = []; - this.rules[7].opcodes[0] = { type: 1, children: [1,2,3,4,5] };// ALT - this.rules[7].opcodes[1] = { type: 4, index: 8 };// RNM(unreserved) - this.rules[7].opcodes[2] = { type: 4, index: 9 };// RNM(pct-encoded) - this.rules[7].opcodes[3] = { type: 4, index: 10 };// RNM(sub-delims) - this.rules[7].opcodes[4] = { type: 7, string: [58] };// TLS - this.rules[7].opcodes[5] = { type: 7, string: [64] };// TLS + this.rules[6].opcodes = []; + this.rules[6].opcodes[0] = { type: 1, children: [1,2,3,4,5] };// ALT + this.rules[6].opcodes[1] = { type: 4, index: 7 };// RNM(unreserved) + this.rules[6].opcodes[2] = { type: 4, index: 8 };// RNM(pct-encoded) + this.rules[6].opcodes[3] = { type: 4, index: 9 };// RNM(sub-delims) + this.rules[6].opcodes[4] = { type: 7, string: [58] };// TLS + this.rules[6].opcodes[5] = { type: 7, string: [64] };// TLS /* unreserved */ - this.rules[8].opcodes = []; - this.rules[8].opcodes[0] = { type: 1, children: [1,2,3,4,5,6] };// ALT - this.rules[8].opcodes[1] = { type: 4, index: 11 };// RNM(ALPHA) - this.rules[8].opcodes[2] = { type: 4, index: 12 };// RNM(DIGIT) - this.rules[8].opcodes[3] = { type: 7, string: [45] };// TLS - this.rules[8].opcodes[4] = { type: 7, string: [46] };// TLS - this.rules[8].opcodes[5] = { type: 7, string: [95] };// TLS - this.rules[8].opcodes[6] = { type: 7, string: [126] };// TLS + this.rules[7].opcodes = []; + this.rules[7].opcodes[0] = { type: 1, children: [1,2,3,4,5,6] };// ALT + this.rules[7].opcodes[1] = { type: 4, index: 10 };// RNM(ALPHA) + this.rules[7].opcodes[2] = { type: 4, index: 11 };// RNM(DIGIT) + this.rules[7].opcodes[3] = { type: 7, string: [45] };// TLS + this.rules[7].opcodes[4] = { type: 7, string: [46] };// TLS + this.rules[7].opcodes[5] = { type: 7, string: [95] };// TLS + this.rules[7].opcodes[6] = { type: 7, string: [126] };// TLS /* pct-encoded */ - this.rules[9].opcodes = []; - this.rules[9].opcodes[0] = { type: 2, children: [1,2,3] };// CAT - this.rules[9].opcodes[1] = { type: 7, string: [37] };// TLS - this.rules[9].opcodes[2] = { type: 4, index: 13 };// RNM(HEXDIG) - this.rules[9].opcodes[3] = { type: 4, index: 13 };// RNM(HEXDIG) + this.rules[8].opcodes = []; + this.rules[8].opcodes[0] = { type: 2, children: [1,2,3] };// CAT + this.rules[8].opcodes[1] = { type: 7, string: [37] };// TLS + this.rules[8].opcodes[2] = { type: 4, index: 12 };// RNM(HEXDIG) + this.rules[8].opcodes[3] = { type: 4, index: 12 };// RNM(HEXDIG) /* sub-delims */ - this.rules[10].opcodes = []; - this.rules[10].opcodes[0] = { type: 1, children: [1,2,3,4,5,6,7,8,9,10,11] };// ALT - this.rules[10].opcodes[1] = { type: 7, string: [33] };// TLS - this.rules[10].opcodes[2] = { type: 7, string: [36] };// TLS - this.rules[10].opcodes[3] = { type: 7, string: [38] };// TLS - this.rules[10].opcodes[4] = { type: 7, string: [39] };// TLS - this.rules[10].opcodes[5] = { type: 7, string: [40] };// TLS - this.rules[10].opcodes[6] = { type: 7, string: [41] };// TLS - this.rules[10].opcodes[7] = { type: 7, string: [42] };// TLS - this.rules[10].opcodes[8] = { type: 7, string: [43] };// TLS - this.rules[10].opcodes[9] = { type: 7, string: [44] };// TLS - this.rules[10].opcodes[10] = { type: 7, string: [59] };// TLS - this.rules[10].opcodes[11] = { type: 7, string: [61] };// TLS + this.rules[9].opcodes = []; + this.rules[9].opcodes[0] = { type: 1, children: [1,2,3,4,5,6,7,8,9,10,11] };// ALT + this.rules[9].opcodes[1] = { type: 7, string: [33] };// TLS + this.rules[9].opcodes[2] = { type: 7, string: [36] };// TLS + this.rules[9].opcodes[3] = { type: 7, string: [38] };// TLS + this.rules[9].opcodes[4] = { type: 7, string: [39] };// TLS + this.rules[9].opcodes[5] = { type: 7, string: [40] };// TLS + this.rules[9].opcodes[6] = { type: 7, string: [41] };// TLS + this.rules[9].opcodes[7] = { type: 7, string: [42] };// TLS + this.rules[9].opcodes[8] = { type: 7, string: [43] };// TLS + this.rules[9].opcodes[9] = { type: 7, string: [44] };// TLS + this.rules[9].opcodes[10] = { type: 7, string: [59] };// TLS + this.rules[9].opcodes[11] = { type: 7, string: [61] };// TLS /* ALPHA */ - this.rules[11].opcodes = []; - this.rules[11].opcodes[0] = { type: 1, children: [1,2] };// ALT - this.rules[11].opcodes[1] = { type: 5, min: 65, max: 90 };// TRG - this.rules[11].opcodes[2] = { type: 5, min: 97, max: 122 };// TRG + this.rules[10].opcodes = []; + this.rules[10].opcodes[0] = { type: 1, children: [1,2] };// ALT + this.rules[10].opcodes[1] = { type: 5, min: 65, max: 90 };// TRG + this.rules[10].opcodes[2] = { type: 5, min: 97, max: 122 };// TRG /* DIGIT */ - this.rules[12].opcodes = []; - this.rules[12].opcodes[0] = { type: 5, min: 48, max: 57 };// TRG + this.rules[11].opcodes = []; + this.rules[11].opcodes[0] = { type: 5, min: 48, max: 57 };// TRG /* HEXDIG */ - this.rules[13].opcodes = []; - this.rules[13].opcodes[0] = { type: 1, children: [1,2,3,4,5,6,7] };// ALT - this.rules[13].opcodes[1] = { type: 4, index: 12 };// RNM(DIGIT) - this.rules[13].opcodes[2] = { type: 7, string: [97] };// TLS - this.rules[13].opcodes[3] = { type: 7, string: [98] };// TLS - this.rules[13].opcodes[4] = { type: 7, string: [99] };// TLS - this.rules[13].opcodes[5] = { type: 7, string: [100] };// TLS - this.rules[13].opcodes[6] = { type: 7, string: [101] };// TLS - this.rules[13].opcodes[7] = { type: 7, string: [102] };// TLS + this.rules[12].opcodes = []; + this.rules[12].opcodes[0] = { type: 1, children: [1,2,3,4,5,6,7] };// ALT + this.rules[12].opcodes[1] = { type: 4, index: 11 };// RNM(DIGIT) + this.rules[12].opcodes[2] = { type: 7, string: [97] };// TLS + this.rules[12].opcodes[3] = { type: 7, string: [98] };// TLS + this.rules[12].opcodes[4] = { type: 7, string: [99] };// TLS + this.rules[12].opcodes[5] = { type: 7, string: [100] };// TLS + this.rules[12].opcodes[6] = { type: 7, string: [101] };// TLS + this.rules[12].opcodes[7] = { type: 7, string: [102] };// TLS // The `toString()` function will display the original grammar file(s) that produced these opcodes. this.toString = function toString(){ let str = ""; str += "; OpenAPI Path Templating ABNF syntax\n"; - str += "; Aligned with RFC 3986 (https://datatracker.ietf.org/doc/html/rfc3986#section-3.3)\n"; - str += "path-template = slash [ path-template-nz ]\n"; - str += "path-template-nz = path-segment *( slash path-segment )\n"; - str += "slash = \"/\"\n"; + str += "path-template = slash *( path-segment slash ) [ path-segment ]\n"; str += "path-segment = 1*( path-literal / template-expression )\n"; + str += "slash = \"/\"\n"; str += "path-literal = 1*pchar\n"; str += "template-expression = \"{\" template-expression-param-name \"}\"\n"; str += "template-expression-param-name = 1*pchar\n"; diff --git a/test/parse.js b/test/parse.js index 514f5ce..4c1e4bd 100644 --- a/test/parse.js +++ b/test/parse.js @@ -40,6 +40,24 @@ describe('parse', function () { }); }); + context('/{petId}/', function () { + specify.only('should parse and translate', function () { + const parseResult = parse('/{petId}/'); + + const parts = []; + parseResult.ast.translate(parts); + + assert.isTrue(parseResult.result.success); + assert.deepEqual(parts, [ + ['path-template', '/{petId}/'], + ['slash', '/'], + ['template-expression', '{petId}'], + ['template-expression-param-name', 'petId'], + ['slash', '/'], + ]); + }); + }); + context('/a{petId}', function () { specify('should parse and translate', function () { const parseResult = parse('/a{petId}'); @@ -135,9 +153,9 @@ describe('parse', function () { context('given invalid source string', function () { context('given empty value for template expression', function () { specify('should fail parsing', function () { - const parseResult = parse('/pets/{}'); + const parseResult = parse('/pets/{petId}/'); - assert.isFalse(parseResult.result.success); + assert.isTrue(parseResult.result.success); }); }); From fdb67cd62c75696f7da17ed29b1ad40c4852ef57 Mon Sep 17 00:00:00 2001 From: Vladimir Gorej Date: Sun, 22 Dec 2024 14:39:42 +0100 Subject: [PATCH 4/8] feat: align grammar with OpenAPI specification Refs https://github.com/OAI/OpenAPI-Specification/pull/4244 --- src/path-templating.bnf | 4 +++- src/path-templating.js | 21 +++++++++++++-------- test/parse.js | 2 +- 3 files changed, 17 insertions(+), 10 deletions(-) diff --git a/src/path-templating.bnf b/src/path-templating.bnf index 4c64eb8..e06fbee 100644 --- a/src/path-templating.bnf +++ b/src/path-templating.bnf @@ -4,7 +4,7 @@ path-segment = 1*( path-literal / template-expression ) slash = "/" path-literal = 1*pchar template-expression = "{" template-expression-param-name "}" -template-expression-param-name = 1*pchar +template-expression-param-name = 1*( %x00-79 / %x7C / %x7E-10FFFF ) ; every UTF8 character except { and } (from OpenAPI) ; Characters definitions (from RFC 3986) pchar = unreserved / pct-encoded / sub-delims / ":" / "@" @@ -12,6 +12,8 @@ unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~" pct-encoded = "%" HEXDIG HEXDIG sub-delims = "!" / "$" / "&" / "'" / "(" / ")" / "*" / "+" / "," / ";" / "=" + +; Character definitions (from RFC 5234) ALPHA = %x41-5A / %x61-7A ; A-Z / a-z DIGIT = %x30-39 ; 0-9 HEXDIG = DIGIT / "A" / "B" / "C" / "D" / "E" / "F" diff --git a/src/path-templating.js b/src/path-templating.js index 37dd74b..a0bc75b 100644 --- a/src/path-templating.js +++ b/src/path-templating.js @@ -7,20 +7,20 @@ export default function grammar(){ // SUMMARY // rules = 13 // udts = 0 - // opcodes = 62 + // opcodes = 65 // --- ABNF original opcodes - // ALT = 6 + // ALT = 7 // CAT = 4 // REP = 5 - // RNM = 17 + // RNM = 16 // TLS = 27 - // TBS = 0 - // TRG = 3 + // TBS = 1 + // TRG = 5 // --- SABNF superset opcodes // UDT = 0 // AND = 0 // NOT = 0 - // characters = [33 - 126] + // characters = [0 - 1114111] // ``` /* OBJECT IDENTIFIER (for internal parser use) */ this.grammarObject = 'grammarObject'; @@ -82,7 +82,10 @@ export default function grammar(){ /* template-expression-param-name */ this.rules[5].opcodes = []; this.rules[5].opcodes[0] = { type: 3, min: 1, max: Infinity };// REP - this.rules[5].opcodes[1] = { type: 4, index: 6 };// RNM(pchar) + this.rules[5].opcodes[1] = { type: 1, children: [2,3,4] };// ALT + this.rules[5].opcodes[2] = { type: 5, min: 0, max: 121 };// TRG + this.rules[5].opcodes[3] = { type: 6, string: [124] };// TBS + this.rules[5].opcodes[4] = { type: 5, min: 126, max: 1114111 };// TRG /* pchar */ this.rules[6].opcodes = []; @@ -155,7 +158,7 @@ export default function grammar(){ str += "slash = \"/\"\n"; str += "path-literal = 1*pchar\n"; str += "template-expression = \"{\" template-expression-param-name \"}\"\n"; - str += "template-expression-param-name = 1*pchar\n"; + str += "template-expression-param-name = 1*( %x00-79 / %x7C / %x7E-10FFFF ) ; every UTF8 character except { and } (from OpenAPI)\n"; str += "\n"; str += "; Characters definitions (from RFC 3986)\n"; str += "pchar = unreserved / pct-encoded / sub-delims / \":\" / \"@\"\n"; @@ -163,6 +166,8 @@ export default function grammar(){ str += "pct-encoded = \"%\" HEXDIG HEXDIG\n"; str += "sub-delims = \"!\" / \"$\" / \"&\" / \"'\" / \"(\" / \")\"\n"; str += " / \"*\" / \"+\" / \",\" / \";\" / \"=\"\n"; + str += "\n"; + str += "; Character definitions (from RFC 5234)\n"; str += "ALPHA = %x41-5A / %x61-7A ; A-Z / a-z\n"; str += "DIGIT = %x30-39 ; 0-9\n"; str += "HEXDIG = DIGIT / \"A\" / \"B\" / \"C\" / \"D\" / \"E\" / \"F\"\n"; diff --git a/test/parse.js b/test/parse.js index 4c1e4bd..4b680d4 100644 --- a/test/parse.js +++ b/test/parse.js @@ -41,7 +41,7 @@ describe('parse', function () { }); context('/{petId}/', function () { - specify.only('should parse and translate', function () { + specify('should parse and translate', function () { const parseResult = parse('/{petId}/'); const parts = []; From 8556b1ece62c140d2cdb566e59e511e4ac9bf574 Mon Sep 17 00:00:00 2001 From: Vladimir Gorej Date: Sun, 22 Dec 2024 14:47:43 +0100 Subject: [PATCH 5/8] docs(README): use description of Path Templating from OpenAPI spec --- NOTICE | 6 ++++++ README.md | 7 ++++--- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/NOTICE b/NOTICE index 429d448..b75fa7b 100644 --- a/NOTICE +++ b/NOTICE @@ -2,3 +2,9 @@ openapi-path-templating Copyright 2023, Vladimír Gorej openapi-path-templating is licensed under Apache 2.0 license. Copy of the Apache 2.0 license can be found in `LICENSE` file. + +OpenAPI Specification 3.1.x +Copyright The Linux Foundation +Fragments of the specification text are embedded in `README.md`. +Definition of `template-expression-param-name` ABNF non-terminal is based on the OpenAPI Specification ABNF. +Copy of the Apache 2.0 license can be found in `LICENSE` file. diff --git a/README.md b/README.md index 0149897..1a9dc2d 100644 --- a/README.md +++ b/README.md @@ -7,9 +7,10 @@ [![try on RunKit](https://img.shields.io/badge/try%20on-RunKit-brightgreen.svg?style=flat)](https://npm.runkit.com/openapi-path-templating) [![Tidelift](https://tidelift.com/badges/package/npm/openapi-path-templating)](https://tidelift.com/subscription/pkg/npm-openapi-path-templating?utm_source=npm-openapi-path-templating&utm_medium=referral&utm_campaign=readme) -[Path Templating](https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.1.0.md#pathTemplating) allow defining values based on information that will only be available within the HTTP message in an actual API call. -This mechanism is used by [Paths Object](https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.1.0.md#paths-object) -of [OpenAPI specification](https://github.com/OAI/OpenAPI-Specification). +[OpenAPI Path Templating](https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.1.1.md#path-templating) refers to the usage of template expressions, delimited by curly braces (`{}`), to mark a section of a URL path as replaceable using path parameters. + +Each template expression in the path MUST correspond to a path parameter that is included in the [Path Item](https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.1.1.md#path-item-object) itself and/or in each of the Path Item's [Operations](https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.1.1.md#operation-object). +An exception is if the path item is empty, for example due to ACL constraints, matching path parameters are not required. `openapi-path-templating` is a **parser**, **validator** and **resolver** for OpenAPI Path Templating. It supports Path Templating defined in following OpenAPI specification versions: From 98929489df968c15c46262e3cbda758ff243a821 Mon Sep 17 00:00:00 2001 From: Vladimir Gorej Date: Sun, 22 Dec 2024 14:48:34 +0100 Subject: [PATCH 6/8] docs(grammar): fix typo --- src/path-templating.bnf | 2 +- src/path-templating.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/path-templating.bnf b/src/path-templating.bnf index e06fbee..2f210c7 100644 --- a/src/path-templating.bnf +++ b/src/path-templating.bnf @@ -13,7 +13,7 @@ pct-encoded = "%" HEXDIG HEXDIG sub-delims = "!" / "$" / "&" / "'" / "(" / ")" / "*" / "+" / "," / ";" / "=" -; Character definitions (from RFC 5234) +; Characters definitions (from RFC 5234) ALPHA = %x41-5A / %x61-7A ; A-Z / a-z DIGIT = %x30-39 ; 0-9 HEXDIG = DIGIT / "A" / "B" / "C" / "D" / "E" / "F" diff --git a/src/path-templating.js b/src/path-templating.js index a0bc75b..a7e8462 100644 --- a/src/path-templating.js +++ b/src/path-templating.js @@ -167,7 +167,7 @@ export default function grammar(){ str += "sub-delims = \"!\" / \"$\" / \"&\" / \"'\" / \"(\" / \")\"\n"; str += " / \"*\" / \"+\" / \",\" / \";\" / \"=\"\n"; str += "\n"; - str += "; Character definitions (from RFC 5234)\n"; + str += "; Characters definitions (from RFC 5234)\n"; str += "ALPHA = %x41-5A / %x61-7A ; A-Z / a-z\n"; str += "DIGIT = %x30-39 ; 0-9\n"; str += "HEXDIG = DIGIT / \"A\" / \"B\" / \"C\" / \"D\" / \"E\" / \"F\"\n"; From 201a04ebca029ab53b8603194385856c05d6a97a Mon Sep 17 00:00:00 2001 From: Vladimir Gorej Date: Sun, 22 Dec 2024 14:54:44 +0100 Subject: [PATCH 7/8] docs(README): mention foundational role for official OpenAPI ABNF --- README.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 1a9dc2d..b9856d5 100644 --- a/README.md +++ b/README.md @@ -8,12 +8,13 @@ [![Tidelift](https://tidelift.com/badges/package/npm/openapi-path-templating)](https://tidelift.com/subscription/pkg/npm-openapi-path-templating?utm_source=npm-openapi-path-templating&utm_medium=referral&utm_campaign=readme) [OpenAPI Path Templating](https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.1.1.md#path-templating) refers to the usage of template expressions, delimited by curly braces (`{}`), to mark a section of a URL path as replaceable using path parameters. - Each template expression in the path MUST correspond to a path parameter that is included in the [Path Item](https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.1.1.md#path-item-object) itself and/or in each of the Path Item's [Operations](https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.1.1.md#operation-object). An exception is if the path item is empty, for example due to ACL constraints, matching path parameters are not required. -`openapi-path-templating` is a **parser**, **validator** and **resolver** for OpenAPI Path Templating. It supports -Path Templating defined in following OpenAPI specification versions: +`openapi-path-templating` is a **parser**, **validator**, and **resolver** for OpenAPI Path Templating, +which played a [foundational role](https://github.com/OAI/OpenAPI-Specification/pull/4244) in defining the official ANBF grammar for OpenAPI Path Templating. + +It supports Path Templating defined in following OpenAPI specification versions: - [OpenAPI 2.0](https://github.com/OAI/OpenAPI-Specification/blob/main/versions/2.0.md#pathTemplating) - [OpenAPI 3.0.0](https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.0.md#pathTemplating) From b5f3cbfd8b957fea2cfb350c651cec4d5af4148d Mon Sep 17 00:00:00 2001 From: Vincent Biret Date: Sun, 22 Dec 2024 09:23:45 -0500 Subject: [PATCH 8/8] test: add additional test cases (#145) Signed-off-by: Vincent Biret --- test/test.js | 42 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/test/test.js b/test/test.js index 9194450..b6b87f0 100644 --- a/test/test.js +++ b/test/test.js @@ -5,22 +5,64 @@ import { test } from '../src/index.js'; describe('test', function () { it('should detect as path template', function () { assert.isTrue(test('/{path}')); + assert.isTrue(test('/{path}/')); // trailing slash is allowed assert.isTrue(test('/pets/{petId}')); assert.isTrue(test('/{entity}/me')); assert.isTrue(test('/books/{id}')); assert.isTrue(test('/a{test}')); + assert.isTrue(test('/{test}a')); + assert.isTrue(test('/range({x},{y})')); // parentheses are allowed + assert.isTrue(test('/range({x},{y})/secondRange({x},{y})')); // repeated parameter names are allowed assert.isTrue(test('/{entity}/{another-entity}/me')); + // some special characters in literals are allowed assert.isTrue(test('/')); + assert.isTrue(test('/-/')); + assert.isTrue(test('/~/')); + assert.isTrue(test('/%20')); + assert.isTrue(test('/functions/t_Dist_2T')); + assert.isTrue(test('/users/$count')); + assert.isTrue(test('/users/delta()')); + assert.isTrue(test('/directoryObjects/microsoft.graph.user')); + assert.isTrue(test("/applications(appId='{appId}')")); + assert.isTrue(test("/com/on/get(meetingOrganizerUserId='@meetingOrganizerUserId')")); + // some special characters in parameters names are allowed + assert.isTrue(test('/users/{user-id}')); + assert.isTrue(test('/{❤️}')); + assert.isTrue(test('/{%}')); + // RFC 6570 operators + assert.isTrue(test('/{y,x}'), '/{y,x}'); + assert.isTrue(test('/{count*}')); }); it('should not detect expression', function () { assert.isFalse(test('')); assert.isFalse(test('1')); + assert.isFalse(test('//')); assert.isFalse(test('{petId}')); assert.isFalse(test('/pet/{petId')); assert.isFalse(test('/pets?offset=0&limit=10')); assert.isFalse(test('/pets?offset={offset}&limit={limit}')); assert.isFalse(test('/pets?offset{offset}limit={limit}')); + assert.isFalse(test('/#baz')); + // special characters in literals are not allowed + assert.isFalse(test('/❤️')); + // special characters in parameters names are not allowed + assert.isFalse(test('/{foo:baz}')); + assert.isFalse(test('/{=baz}')); + assert.isFalse(test('/{$baz}')); + assert.isFalse(test('/{~baz}')); + assert.isFalse(test('/{#baz}')); + assert.isFalse(test('/{?baz}')); + assert.isFalse(test('/{/baz}')); + assert.isFalse(test('/{foo baz}')); + assert.isFalse(test('/{|baz}')); + assert.isFalse(test('/{^baz}')); + assert.isFalse(test('/{`baz}')); + // RFC 6570 operators + assert.isFalse(test('/{;baz}')); + assert.isFalse(test('/{&baz}')); + assert.isFalse(test('/{.baz}')); + // invalid types assert.isFalse(test(1)); assert.isFalse(test(null)); assert.isFalse(test(undefined));