Skip to content

Commit b2fa107

Browse files
committed
fix: return support for trailing slash
1 parent 23d39ac commit b2fa107

File tree

3 files changed

+109
-101
lines changed

3 files changed

+109
-101
lines changed

src/path-templating.bnf

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,7 @@
11
; OpenAPI Path Templating ABNF syntax
2-
; Aligned with RFC 3986 (https://datatracker.ietf.org/doc/html/rfc3986#section-3.3)
3-
path-template = slash [ path-template-nz ]
4-
path-template-nz = path-segment *( slash path-segment )
5-
slash = "/"
2+
path-template = slash *( path-segment slash ) [ path-segment ]
63
path-segment = 1*( path-literal / template-expression )
4+
slash = "/"
75
path-literal = 1*pchar
86
template-expression = "{" template-expression-param-name "}"
97
template-expression-param-name = 1*pchar

src/path-templating.js

Lines changed: 87 additions & 95 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,14 @@
55
export default function grammar(){
66
// ```
77
// SUMMARY
8-
// rules = 14
8+
// rules = 13
99
// udts = 0
10-
// opcodes = 64
10+
// opcodes = 62
1111
// --- ABNF original opcodes
1212
// ALT = 6
13-
// CAT = 5
13+
// CAT = 4
1414
// REP = 5
15-
// RNM = 18
15+
// RNM = 17
1616
// TLS = 27
1717
// TBS = 0
1818
// TRG = 3
@@ -28,139 +28,131 @@ export default function grammar(){
2828
/* RULES */
2929
this.rules = [];
3030
this.rules[0] = { name: 'path-template', lower: 'path-template', index: 0, isBkr: false };
31-
this.rules[1] = { name: 'path-template-nz', lower: 'path-template-nz', index: 1, isBkr: false };
31+
this.rules[1] = { name: 'path-segment', lower: 'path-segment', index: 1, isBkr: false };
3232
this.rules[2] = { name: 'slash', lower: 'slash', index: 2, isBkr: false };
33-
this.rules[3] = { name: 'path-segment', lower: 'path-segment', index: 3, isBkr: false };
34-
this.rules[4] = { name: 'path-literal', lower: 'path-literal', index: 4, isBkr: false };
35-
this.rules[5] = { name: 'template-expression', lower: 'template-expression', index: 5, isBkr: false };
36-
this.rules[6] = { name: 'template-expression-param-name', lower: 'template-expression-param-name', index: 6, isBkr: false };
37-
this.rules[7] = { name: 'pchar', lower: 'pchar', index: 7, isBkr: false };
38-
this.rules[8] = { name: 'unreserved', lower: 'unreserved', index: 8, isBkr: false };
39-
this.rules[9] = { name: 'pct-encoded', lower: 'pct-encoded', index: 9, isBkr: false };
40-
this.rules[10] = { name: 'sub-delims', lower: 'sub-delims', index: 10, isBkr: false };
41-
this.rules[11] = { name: 'ALPHA', lower: 'alpha', index: 11, isBkr: false };
42-
this.rules[12] = { name: 'DIGIT', lower: 'digit', index: 12, isBkr: false };
43-
this.rules[13] = { name: 'HEXDIG', lower: 'hexdig', index: 13, isBkr: false };
33+
this.rules[3] = { name: 'path-literal', lower: 'path-literal', index: 3, isBkr: false };
34+
this.rules[4] = { name: 'template-expression', lower: 'template-expression', index: 4, isBkr: false };
35+
this.rules[5] = { name: 'template-expression-param-name', lower: 'template-expression-param-name', index: 5, isBkr: false };
36+
this.rules[6] = { name: 'pchar', lower: 'pchar', index: 6, isBkr: false };
37+
this.rules[7] = { name: 'unreserved', lower: 'unreserved', index: 7, isBkr: false };
38+
this.rules[8] = { name: 'pct-encoded', lower: 'pct-encoded', index: 8, isBkr: false };
39+
this.rules[9] = { name: 'sub-delims', lower: 'sub-delims', index: 9, isBkr: false };
40+
this.rules[10] = { name: 'ALPHA', lower: 'alpha', index: 10, isBkr: false };
41+
this.rules[11] = { name: 'DIGIT', lower: 'digit', index: 11, isBkr: false };
42+
this.rules[12] = { name: 'HEXDIG', lower: 'hexdig', index: 12, isBkr: false };
4443

4544
/* UDTS */
4645
this.udts = [];
4746

4847
/* OPCODES */
4948
/* path-template */
5049
this.rules[0].opcodes = [];
51-
this.rules[0].opcodes[0] = { type: 2, children: [1,2] };// CAT
50+
this.rules[0].opcodes[0] = { type: 2, children: [1,2,6] };// CAT
5251
this.rules[0].opcodes[1] = { type: 4, index: 2 };// RNM(slash)
53-
this.rules[0].opcodes[2] = { type: 3, min: 0, max: 1 };// REP
54-
this.rules[0].opcodes[3] = { type: 4, index: 1 };// RNM(path-template-nz)
52+
this.rules[0].opcodes[2] = { type: 3, min: 0, max: Infinity };// REP
53+
this.rules[0].opcodes[3] = { type: 2, children: [4,5] };// CAT
54+
this.rules[0].opcodes[4] = { type: 4, index: 1 };// RNM(path-segment)
55+
this.rules[0].opcodes[5] = { type: 4, index: 2 };// RNM(slash)
56+
this.rules[0].opcodes[6] = { type: 3, min: 0, max: 1 };// REP
57+
this.rules[0].opcodes[7] = { type: 4, index: 1 };// RNM(path-segment)
5558

56-
/* path-template-nz */
59+
/* path-segment */
5760
this.rules[1].opcodes = [];
58-
this.rules[1].opcodes[0] = { type: 2, children: [1,2] };// CAT
59-
this.rules[1].opcodes[1] = { type: 4, index: 3 };// RNM(path-segment)
60-
this.rules[1].opcodes[2] = { type: 3, min: 0, max: Infinity };// REP
61-
this.rules[1].opcodes[3] = { type: 2, children: [4,5] };// CAT
62-
this.rules[1].opcodes[4] = { type: 4, index: 2 };// RNM(slash)
63-
this.rules[1].opcodes[5] = { type: 4, index: 3 };// RNM(path-segment)
61+
this.rules[1].opcodes[0] = { type: 3, min: 1, max: Infinity };// REP
62+
this.rules[1].opcodes[1] = { type: 1, children: [2,3] };// ALT
63+
this.rules[1].opcodes[2] = { type: 4, index: 3 };// RNM(path-literal)
64+
this.rules[1].opcodes[3] = { type: 4, index: 4 };// RNM(template-expression)
6465

6566
/* slash */
6667
this.rules[2].opcodes = [];
6768
this.rules[2].opcodes[0] = { type: 7, string: [47] };// TLS
6869

69-
/* path-segment */
70+
/* path-literal */
7071
this.rules[3].opcodes = [];
7172
this.rules[3].opcodes[0] = { type: 3, min: 1, max: Infinity };// REP
72-
this.rules[3].opcodes[1] = { type: 1, children: [2,3] };// ALT
73-
this.rules[3].opcodes[2] = { type: 4, index: 4 };// RNM(path-literal)
74-
this.rules[3].opcodes[3] = { type: 4, index: 5 };// RNM(template-expression)
75-
76-
/* path-literal */
77-
this.rules[4].opcodes = [];
78-
this.rules[4].opcodes[0] = { type: 3, min: 1, max: Infinity };// REP
79-
this.rules[4].opcodes[1] = { type: 4, index: 7 };// RNM(pchar)
73+
this.rules[3].opcodes[1] = { type: 4, index: 6 };// RNM(pchar)
8074

8175
/* template-expression */
82-
this.rules[5].opcodes = [];
83-
this.rules[5].opcodes[0] = { type: 2, children: [1,2,3] };// CAT
84-
this.rules[5].opcodes[1] = { type: 7, string: [123] };// TLS
85-
this.rules[5].opcodes[2] = { type: 4, index: 6 };// RNM(template-expression-param-name)
86-
this.rules[5].opcodes[3] = { type: 7, string: [125] };// TLS
76+
this.rules[4].opcodes = [];
77+
this.rules[4].opcodes[0] = { type: 2, children: [1,2,3] };// CAT
78+
this.rules[4].opcodes[1] = { type: 7, string: [123] };// TLS
79+
this.rules[4].opcodes[2] = { type: 4, index: 5 };// RNM(template-expression-param-name)
80+
this.rules[4].opcodes[3] = { type: 7, string: [125] };// TLS
8781

8882
/* template-expression-param-name */
89-
this.rules[6].opcodes = [];
90-
this.rules[6].opcodes[0] = { type: 3, min: 1, max: Infinity };// REP
91-
this.rules[6].opcodes[1] = { type: 4, index: 7 };// RNM(pchar)
83+
this.rules[5].opcodes = [];
84+
this.rules[5].opcodes[0] = { type: 3, min: 1, max: Infinity };// REP
85+
this.rules[5].opcodes[1] = { type: 4, index: 6 };// RNM(pchar)
9286

9387
/* pchar */
94-
this.rules[7].opcodes = [];
95-
this.rules[7].opcodes[0] = { type: 1, children: [1,2,3,4,5] };// ALT
96-
this.rules[7].opcodes[1] = { type: 4, index: 8 };// RNM(unreserved)
97-
this.rules[7].opcodes[2] = { type: 4, index: 9 };// RNM(pct-encoded)
98-
this.rules[7].opcodes[3] = { type: 4, index: 10 };// RNM(sub-delims)
99-
this.rules[7].opcodes[4] = { type: 7, string: [58] };// TLS
100-
this.rules[7].opcodes[5] = { type: 7, string: [64] };// TLS
88+
this.rules[6].opcodes = [];
89+
this.rules[6].opcodes[0] = { type: 1, children: [1,2,3,4,5] };// ALT
90+
this.rules[6].opcodes[1] = { type: 4, index: 7 };// RNM(unreserved)
91+
this.rules[6].opcodes[2] = { type: 4, index: 8 };// RNM(pct-encoded)
92+
this.rules[6].opcodes[3] = { type: 4, index: 9 };// RNM(sub-delims)
93+
this.rules[6].opcodes[4] = { type: 7, string: [58] };// TLS
94+
this.rules[6].opcodes[5] = { type: 7, string: [64] };// TLS
10195

10296
/* unreserved */
103-
this.rules[8].opcodes = [];
104-
this.rules[8].opcodes[0] = { type: 1, children: [1,2,3,4,5,6] };// ALT
105-
this.rules[8].opcodes[1] = { type: 4, index: 11 };// RNM(ALPHA)
106-
this.rules[8].opcodes[2] = { type: 4, index: 12 };// RNM(DIGIT)
107-
this.rules[8].opcodes[3] = { type: 7, string: [45] };// TLS
108-
this.rules[8].opcodes[4] = { type: 7, string: [46] };// TLS
109-
this.rules[8].opcodes[5] = { type: 7, string: [95] };// TLS
110-
this.rules[8].opcodes[6] = { type: 7, string: [126] };// TLS
97+
this.rules[7].opcodes = [];
98+
this.rules[7].opcodes[0] = { type: 1, children: [1,2,3,4,5,6] };// ALT
99+
this.rules[7].opcodes[1] = { type: 4, index: 10 };// RNM(ALPHA)
100+
this.rules[7].opcodes[2] = { type: 4, index: 11 };// RNM(DIGIT)
101+
this.rules[7].opcodes[3] = { type: 7, string: [45] };// TLS
102+
this.rules[7].opcodes[4] = { type: 7, string: [46] };// TLS
103+
this.rules[7].opcodes[5] = { type: 7, string: [95] };// TLS
104+
this.rules[7].opcodes[6] = { type: 7, string: [126] };// TLS
111105

112106
/* pct-encoded */
113-
this.rules[9].opcodes = [];
114-
this.rules[9].opcodes[0] = { type: 2, children: [1,2,3] };// CAT
115-
this.rules[9].opcodes[1] = { type: 7, string: [37] };// TLS
116-
this.rules[9].opcodes[2] = { type: 4, index: 13 };// RNM(HEXDIG)
117-
this.rules[9].opcodes[3] = { type: 4, index: 13 };// RNM(HEXDIG)
107+
this.rules[8].opcodes = [];
108+
this.rules[8].opcodes[0] = { type: 2, children: [1,2,3] };// CAT
109+
this.rules[8].opcodes[1] = { type: 7, string: [37] };// TLS
110+
this.rules[8].opcodes[2] = { type: 4, index: 12 };// RNM(HEXDIG)
111+
this.rules[8].opcodes[3] = { type: 4, index: 12 };// RNM(HEXDIG)
118112

119113
/* sub-delims */
120-
this.rules[10].opcodes = [];
121-
this.rules[10].opcodes[0] = { type: 1, children: [1,2,3,4,5,6,7,8,9,10,11] };// ALT
122-
this.rules[10].opcodes[1] = { type: 7, string: [33] };// TLS
123-
this.rules[10].opcodes[2] = { type: 7, string: [36] };// TLS
124-
this.rules[10].opcodes[3] = { type: 7, string: [38] };// TLS
125-
this.rules[10].opcodes[4] = { type: 7, string: [39] };// TLS
126-
this.rules[10].opcodes[5] = { type: 7, string: [40] };// TLS
127-
this.rules[10].opcodes[6] = { type: 7, string: [41] };// TLS
128-
this.rules[10].opcodes[7] = { type: 7, string: [42] };// TLS
129-
this.rules[10].opcodes[8] = { type: 7, string: [43] };// TLS
130-
this.rules[10].opcodes[9] = { type: 7, string: [44] };// TLS
131-
this.rules[10].opcodes[10] = { type: 7, string: [59] };// TLS
132-
this.rules[10].opcodes[11] = { type: 7, string: [61] };// TLS
114+
this.rules[9].opcodes = [];
115+
this.rules[9].opcodes[0] = { type: 1, children: [1,2,3,4,5,6,7,8,9,10,11] };// ALT
116+
this.rules[9].opcodes[1] = { type: 7, string: [33] };// TLS
117+
this.rules[9].opcodes[2] = { type: 7, string: [36] };// TLS
118+
this.rules[9].opcodes[3] = { type: 7, string: [38] };// TLS
119+
this.rules[9].opcodes[4] = { type: 7, string: [39] };// TLS
120+
this.rules[9].opcodes[5] = { type: 7, string: [40] };// TLS
121+
this.rules[9].opcodes[6] = { type: 7, string: [41] };// TLS
122+
this.rules[9].opcodes[7] = { type: 7, string: [42] };// TLS
123+
this.rules[9].opcodes[8] = { type: 7, string: [43] };// TLS
124+
this.rules[9].opcodes[9] = { type: 7, string: [44] };// TLS
125+
this.rules[9].opcodes[10] = { type: 7, string: [59] };// TLS
126+
this.rules[9].opcodes[11] = { type: 7, string: [61] };// TLS
133127

134128
/* ALPHA */
135-
this.rules[11].opcodes = [];
136-
this.rules[11].opcodes[0] = { type: 1, children: [1,2] };// ALT
137-
this.rules[11].opcodes[1] = { type: 5, min: 65, max: 90 };// TRG
138-
this.rules[11].opcodes[2] = { type: 5, min: 97, max: 122 };// TRG
129+
this.rules[10].opcodes = [];
130+
this.rules[10].opcodes[0] = { type: 1, children: [1,2] };// ALT
131+
this.rules[10].opcodes[1] = { type: 5, min: 65, max: 90 };// TRG
132+
this.rules[10].opcodes[2] = { type: 5, min: 97, max: 122 };// TRG
139133

140134
/* DIGIT */
141-
this.rules[12].opcodes = [];
142-
this.rules[12].opcodes[0] = { type: 5, min: 48, max: 57 };// TRG
135+
this.rules[11].opcodes = [];
136+
this.rules[11].opcodes[0] = { type: 5, min: 48, max: 57 };// TRG
143137

144138
/* HEXDIG */
145-
this.rules[13].opcodes = [];
146-
this.rules[13].opcodes[0] = { type: 1, children: [1,2,3,4,5,6,7] };// ALT
147-
this.rules[13].opcodes[1] = { type: 4, index: 12 };// RNM(DIGIT)
148-
this.rules[13].opcodes[2] = { type: 7, string: [97] };// TLS
149-
this.rules[13].opcodes[3] = { type: 7, string: [98] };// TLS
150-
this.rules[13].opcodes[4] = { type: 7, string: [99] };// TLS
151-
this.rules[13].opcodes[5] = { type: 7, string: [100] };// TLS
152-
this.rules[13].opcodes[6] = { type: 7, string: [101] };// TLS
153-
this.rules[13].opcodes[7] = { type: 7, string: [102] };// TLS
139+
this.rules[12].opcodes = [];
140+
this.rules[12].opcodes[0] = { type: 1, children: [1,2,3,4,5,6,7] };// ALT
141+
this.rules[12].opcodes[1] = { type: 4, index: 11 };// RNM(DIGIT)
142+
this.rules[12].opcodes[2] = { type: 7, string: [97] };// TLS
143+
this.rules[12].opcodes[3] = { type: 7, string: [98] };// TLS
144+
this.rules[12].opcodes[4] = { type: 7, string: [99] };// TLS
145+
this.rules[12].opcodes[5] = { type: 7, string: [100] };// TLS
146+
this.rules[12].opcodes[6] = { type: 7, string: [101] };// TLS
147+
this.rules[12].opcodes[7] = { type: 7, string: [102] };// TLS
154148

155149
// The `toString()` function will display the original grammar file(s) that produced these opcodes.
156150
this.toString = function toString(){
157151
let str = "";
158152
str += "; OpenAPI Path Templating ABNF syntax\n";
159-
str += "; Aligned with RFC 3986 (https://datatracker.ietf.org/doc/html/rfc3986#section-3.3)\n";
160-
str += "path-template = slash [ path-template-nz ]\n";
161-
str += "path-template-nz = path-segment *( slash path-segment )\n";
162-
str += "slash = \"/\"\n";
153+
str += "path-template = slash *( path-segment slash ) [ path-segment ]\n";
163154
str += "path-segment = 1*( path-literal / template-expression )\n";
155+
str += "slash = \"/\"\n";
164156
str += "path-literal = 1*pchar\n";
165157
str += "template-expression = \"{\" template-expression-param-name \"}\"\n";
166158
str += "template-expression-param-name = 1*pchar\n";

test/parse.js

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,24 @@ describe('parse', function () {
4040
});
4141
});
4242

43+
context('/{petId}/', function () {
44+
specify.only('should parse and translate', function () {
45+
const parseResult = parse('/{petId}/');
46+
47+
const parts = [];
48+
parseResult.ast.translate(parts);
49+
50+
assert.isTrue(parseResult.result.success);
51+
assert.deepEqual(parts, [
52+
['path-template', '/{petId}/'],
53+
['slash', '/'],
54+
['template-expression', '{petId}'],
55+
['template-expression-param-name', 'petId'],
56+
['slash', '/'],
57+
]);
58+
});
59+
});
60+
4361
context('/a{petId}', function () {
4462
specify('should parse and translate', function () {
4563
const parseResult = parse('/a{petId}');
@@ -135,9 +153,9 @@ describe('parse', function () {
135153
context('given invalid source string', function () {
136154
context('given empty value for template expression', function () {
137155
specify('should fail parsing', function () {
138-
const parseResult = parse('/pets/{}');
156+
const parseResult = parse('/pets/{petId}/');
139157

140-
assert.isFalse(parseResult.result.success);
158+
assert.isTrue(parseResult.result.success);
141159
});
142160
});
143161

0 commit comments

Comments
 (0)