Skip to content

Commit 2a10911

Browse files
authored
Merge pull request #1181 from shockey/bug/3817-query-allowreserved-encoding
Address issues with allowReserved
2 parents 2649e63 + f5638c9 commit 2a10911

File tree

4 files changed

+234
-22
lines changed

4 files changed

+234
-22
lines changed

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@
6767
"cookie": "^0.3.1",
6868
"cross-fetch": "0.0.8",
6969
"deep-extend": "^0.4.1",
70+
"encode-3986": "^1.0.0",
7071
"fast-json-patch": "1.1.8",
7172
"isomorphic-form-data": "0.0.1",
7273
"js-yaml": "^3.8.1",

src/execute/oas3/parameter-builders.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ function path({req, value, parameter}) {
1414
value,
1515
style: style || 'simple',
1616
explode: explode || false,
17-
escape: !parameter.allowReserved,
17+
escape: false,
1818
})
1919

2020
req.url = req.url.replace(`{${name}}`, styledValue)
@@ -110,7 +110,7 @@ function header({req, parameter, value}) {
110110
value,
111111
style: parameter.style || 'simple',
112112
explode: typeof parameter.explode === 'undefined' ? false : parameter.explode,
113-
escape: !parameter.allowReserved,
113+
escape: false,
114114
})
115115
}
116116
}

src/execute/oas3/style-serializer.js

Lines changed: 25 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import encodeToRFC3986 from 'encode-3986'
2+
13
export default function (config) {
24
const {value} = config
35

@@ -14,16 +16,18 @@ export default function (config) {
1416
const escapeFn = str => encodeURIComponent(str)
1517

1618
function encodeArray({key, value, style, explode, escape}) {
19+
const valueEncoder = escape ? a => encodeToRFC3986(a) : a => a
20+
1721
if (style === 'simple') {
18-
return value.join(',')
22+
return value.map(val => valueEncoder(val)).join(',')
1923
}
2024

2125
if (style === 'label') {
22-
return `.${value.join('.')}`
26+
return `.${value.map(val => valueEncoder(val)).join('.')}`
2327
}
2428

2529
if (style === 'matrix') {
26-
return value.reduce((prev, curr) => {
30+
return value.map(val => valueEncoder(val)).reduce((prev, curr) => {
2731
if (!prev || explode) {
2832
return `${(prev || '')};${key}=${curr}`
2933
}
@@ -34,27 +38,28 @@ function encodeArray({key, value, style, explode, escape}) {
3438
if (style === 'form') {
3539
const commaValue = escape ? escapeFn(',') : ','
3640
const after = explode ? `&${key}=` : commaValue
37-
return value.join(after)
41+
return value.map(val => valueEncoder(val)).join(after)
3842
}
3943

4044
if (style === 'spaceDelimited') {
4145
const after = explode ? `${key}=` : ''
42-
return value.join(`${escapeFn(' ')}${after}`)
46+
return value.map(val => valueEncoder(val)).join(`${escapeFn(' ')}${after}`)
4347
}
4448

4549
if (style === 'pipeDelimited') {
4650
const after = explode ? `${key}=` : ''
4751
const separator = escape ? escapeFn('|') : '|'
48-
return value.join(`${separator}${after}`)
52+
return value.map(val => valueEncoder(val)).join(`${separator}${after}`)
4953
}
5054
}
5155

52-
function encodeObject({key, value, style, explode}) {
56+
function encodeObject({key, value, style, explode, escape}) {
57+
const valueEncoder = escape ? a => encodeToRFC3986(a) : a => a
5358
const valueKeys = Object.keys(value)
5459

5560
if (style === 'simple') {
5661
return valueKeys.reduce((prev, curr) => {
57-
const val = value[curr]
62+
const val = valueEncoder(value[curr])
5863
const middleChar = explode ? '=' : ','
5964
const prefix = prev ? `${prev},` : ''
6065

@@ -64,7 +69,7 @@ function encodeObject({key, value, style, explode}) {
6469

6570
if (style === 'label') {
6671
return valueKeys.reduce((prev, curr) => {
67-
const val = value[curr]
72+
const val = valueEncoder(value[curr])
6873
const middleChar = explode ? '=' : '.'
6974
const prefix = prev ? `${prev}.` : '.'
7075

@@ -74,7 +79,7 @@ function encodeObject({key, value, style, explode}) {
7479

7580
if (style === 'matrix' && explode) {
7681
return valueKeys.reduce((prev, curr) => {
77-
const val = value[curr]
82+
const val = valueEncoder(value[curr])
7883
const prefix = prev ? `${prev};` : ';'
7984

8085
return `${prefix}${curr}=${val}`
@@ -84,7 +89,7 @@ function encodeObject({key, value, style, explode}) {
8489
if (style === 'matrix') {
8590
// no explode
8691
return valueKeys.reduce((prev, curr) => {
87-
const val = value[curr]
92+
const val = valueEncoder(value[curr])
8893
const prefix = prev ? `${prev},` : `;${key}=`
8994

9095
return `${prefix}${curr},${val}`
@@ -93,7 +98,7 @@ function encodeObject({key, value, style, explode}) {
9398

9499
if (style === 'form') {
95100
return valueKeys.reduce((prev, curr) => {
96-
const val = value[curr]
101+
const val = valueEncoder(value[curr])
97102
const prefix = prev ? `${prev}${explode ? '&' : ','}` : ''
98103
const separator = explode ? '=' : ','
99104

@@ -102,24 +107,26 @@ function encodeObject({key, value, style, explode}) {
102107
}
103108
}
104109

105-
function encodePrimitive({key, value, style, explode}) {
110+
function encodePrimitive({key, value, style, explode, escape}) {
111+
const valueEncoder = escape ? a => encodeToRFC3986(a) : a => a
112+
106113
if (style === 'simple') {
107-
return value
114+
return valueEncoder(value)
108115
}
109116

110117
if (style === 'label') {
111-
return `.${value}`
118+
return `.${valueEncoder(value)}`
112119
}
113120

114121
if (style === 'matrix') {
115-
return `;${key}=${value}`
122+
return `;${key}=${valueEncoder(value)}`
116123
}
117124

118125
if (style === 'form') {
119-
return value
126+
return valueEncoder(value)
120127
}
121128

122129
if (style === 'deepObject') {
123-
return value
130+
return valueEncoder(value)
124131
}
125132
}

test/oas3/execute/style-explode.js

Lines changed: 206 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -946,6 +946,83 @@ describe('buildRequest w/ `style` & `explode` - OpenAPI Specification 3.0', func
946946
headers: {},
947947
})
948948
})
949+
950+
it('should build a query parameter in form/no-explode format with allowReserved', function () {
951+
// Given
952+
const spec = {
953+
openapi: '3.0.0',
954+
paths: {
955+
'/users': {
956+
get: {
957+
operationId: 'myOperation',
958+
parameters: [
959+
{
960+
name: 'id',
961+
in: 'query',
962+
style: 'form',
963+
explode: false,
964+
allowReserved: true
965+
}
966+
]
967+
}
968+
}
969+
}
970+
}
971+
972+
// when
973+
const req = buildRequest({
974+
spec,
975+
operationId: 'myOperation',
976+
parameters: {
977+
id: ':/?#[]@!$&\'()*+,;='
978+
}
979+
})
980+
981+
expect(req).toEqual({
982+
method: 'GET',
983+
url: '/users?id=:/?#[]@!$&\'()*+,;=',
984+
credentials: 'same-origin',
985+
headers: {},
986+
})
987+
})
988+
989+
it('should build a query parameter in form/no-explode format with percent-encoding if allowReserved is not set', function () {
990+
// Given
991+
const spec = {
992+
openapi: '3.0.0',
993+
paths: {
994+
'/users': {
995+
get: {
996+
operationId: 'myOperation',
997+
parameters: [
998+
{
999+
name: 'id',
1000+
in: 'query',
1001+
style: 'form',
1002+
explode: false
1003+
}
1004+
]
1005+
}
1006+
}
1007+
}
1008+
}
1009+
1010+
// when
1011+
const req = buildRequest({
1012+
spec,
1013+
operationId: 'myOperation',
1014+
parameters: {
1015+
id: ':/?#[]@!$&\'()*+,;='
1016+
}
1017+
})
1018+
1019+
expect(req).toEqual({
1020+
method: 'GET',
1021+
url: '/users?id=%3A%2F%3F%23%5B%5D%40%21%24%26%27%28%29%2A%2B%2C%3B%3D',
1022+
credentials: 'same-origin',
1023+
headers: {},
1024+
})
1025+
})
9491026
})
9501027
describe('array values', function () {
9511028
const VALUE = [3, 4, 5]
@@ -1089,13 +1166,57 @@ describe('buildRequest w/ `style` & `explode` - OpenAPI Specification 3.0', func
10891166
spec,
10901167
operationId: 'myOperation',
10911168
parameters: {
1092-
id: VALUE
1169+
id: [
1170+
':', '/', '?', '#', '[', ']', '@', '!', '$', '&', '\'',
1171+
'(', ')', '*', '+', ',', ';', '='
1172+
]
1173+
}
1174+
})
1175+
1176+
expect(req).toEqual({
1177+
method: 'GET',
1178+
url: '/users?id=:,/,?,#,[,],@,!,$,&,\',(,),*,+,,,;,=',
1179+
credentials: 'same-origin',
1180+
headers: {},
1181+
})
1182+
})
1183+
1184+
it('should build a query parameter in form/no-explode format without allowReserved', function () {
1185+
// Given
1186+
const spec = {
1187+
openapi: '3.0.0',
1188+
paths: {
1189+
'/users': {
1190+
get: {
1191+
operationId: 'myOperation',
1192+
parameters: [
1193+
{
1194+
name: 'id',
1195+
in: 'query',
1196+
style: 'form',
1197+
explode: false
1198+
}
1199+
]
1200+
}
1201+
}
1202+
}
1203+
}
1204+
1205+
// when
1206+
const req = buildRequest({
1207+
spec,
1208+
operationId: 'myOperation',
1209+
parameters: {
1210+
id: [
1211+
':', '/', '?', '#', '[', ']', '@', '!', '$', '&', '\'',
1212+
'(', ')', '*', '+', ',', ';', '='
1213+
]
10931214
}
10941215
})
10951216

10961217
expect(req).toEqual({
10971218
method: 'GET',
1098-
url: '/users?id=3,4,5',
1219+
url: '/users?id=%3A%2C%2F%2C%3F%2C%23%2C%5B%2C%5D%2C%40%2C%21%2C%24%2C%26%2C%27%2C%28%2C%29%2C%2A%2C%2B%2C%2C%2C%3B%2C%3D',
10991220
credentials: 'same-origin',
11001221
headers: {},
11011222
})
@@ -1488,6 +1609,89 @@ describe('buildRequest w/ `style` & `explode` - OpenAPI Specification 3.0', func
14881609
})
14891610
})
14901611

1612+
it('should build a query parameter in form/no-explode format with allowReserved', function () {
1613+
// Given
1614+
const spec = {
1615+
openapi: '3.0.0',
1616+
paths: {
1617+
'/users': {
1618+
get: {
1619+
operationId: 'myOperation',
1620+
parameters: [
1621+
{
1622+
name: 'id',
1623+
in: 'query',
1624+
style: 'form',
1625+
explode: false,
1626+
allowReserved: true
1627+
}
1628+
]
1629+
}
1630+
}
1631+
}
1632+
}
1633+
1634+
// when
1635+
const req = buildRequest({
1636+
spec,
1637+
operationId: 'myOperation',
1638+
parameters: {
1639+
id: {
1640+
role: 'admin',
1641+
firstName: ':/?#[]@!$&\'()*+,;='
1642+
}
1643+
}
1644+
})
1645+
1646+
expect(req).toEqual({
1647+
method: 'GET',
1648+
url: '/users?id=role,admin,firstName,:/?#[]@!$&\'()*+,;=',
1649+
credentials: 'same-origin',
1650+
headers: {},
1651+
})
1652+
})
1653+
1654+
it('should build a query parameter in form/no-explode format without allowReserved', function () {
1655+
// Given
1656+
const spec = {
1657+
openapi: '3.0.0',
1658+
paths: {
1659+
'/users': {
1660+
get: {
1661+
operationId: 'myOperation',
1662+
parameters: [
1663+
{
1664+
name: 'id',
1665+
in: 'query',
1666+
style: 'form',
1667+
explode: false
1668+
}
1669+
]
1670+
}
1671+
}
1672+
}
1673+
}
1674+
1675+
// when
1676+
const req = buildRequest({
1677+
spec,
1678+
operationId: 'myOperation',
1679+
parameters: {
1680+
id: {
1681+
role: 'admin',
1682+
firstName: ':/?#[]@!$&\'()*+,;='
1683+
}
1684+
}
1685+
})
1686+
1687+
expect(req).toEqual({
1688+
method: 'GET',
1689+
url: '/users?id=role,admin,firstName,%3A%2F%3F%23%5B%5D%40%21%24%26%27%28%29%2A%2B%2C%3B%3D',
1690+
credentials: 'same-origin',
1691+
headers: {},
1692+
})
1693+
})
1694+
14911695
it('should build a query parameter in deepObject/explode format', function () {
14921696
// Given
14931697
const spec = {

0 commit comments

Comments
 (0)