Skip to content

Commit 54f2b56

Browse files
authored
Fixes and enhancements for array query param handling (#241)
1 parent 532fa04 commit 54f2b56

File tree

5 files changed

+173
-10
lines changed

5 files changed

+173
-10
lines changed
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
openapi: 3.0.3
2+
info:
3+
version: 1.0.0
4+
title: ""
5+
servers:
6+
- url: https://example.com
7+
paths:
8+
/Things:
9+
get:
10+
summary: View Things
11+
parameters:
12+
- name: "arrayParam"
13+
in: "query"
14+
required: false
15+
description: "You can pass 0, 1 or 2 occurrences of this in the query string"
16+
style: "form"
17+
explode: true
18+
schema:
19+
type: "array"
20+
maxItems: 2
21+
items:
22+
type: "string"
23+
responses:
24+
"200":
25+
description: OK
26+
/Stuff:
27+
get:
28+
summary: View Stuff
29+
parameters:
30+
- name: "arrayParam"
31+
in: "query"
32+
required: false
33+
description: "You can pass 0, 1 or 2 occurrences of this in the query string"
34+
style: "pipeDelimited"
35+
explode: false
36+
schema:
37+
type: "array"
38+
maxItems: 2
39+
items:
40+
type: "string"
41+
responses:
42+
"200":
43+
description: OK

packages/docusaurus-plugin-openapi/src/openapi/types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -179,7 +179,7 @@ export interface ParameterObject {
179179
allowEmptyValue?: boolean;
180180
//
181181
style?: string;
182-
explode?: string;
182+
explode?: boolean;
183183
allowReserved?: boolean;
184184
schema?: SchemaObject;
185185
example?: any;

packages/docusaurus-theme-openapi/src/theme/ApiDemoPanel/ParamOptions/index.tsx

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
* LICENSE file in the root directory of this source tree.
66
* ========================================================================== */
77

8-
import React, { useState, useEffect } from "react";
8+
import React, { useState } from "react";
99

1010
import { nanoid } from "@reduxjs/toolkit";
1111

@@ -21,6 +21,11 @@ interface ParamProps {
2121
param: Param;
2222
}
2323

24+
interface Item {
25+
id: string;
26+
value?: string;
27+
}
28+
2429
function ParamOption({ param }: ParamProps) {
2530
if (param.schema?.type === "array" && param.schema.items?.enum) {
2631
return <ParamMultiSelectFormItem param={param} />;
@@ -161,10 +166,16 @@ function ArrayItem({
161166
}
162167

163168
function ParamArrayFormItem({ param }: ParamProps) {
164-
const [items, setItems] = useState<{ id: string; value?: string }[]>([]);
169+
const [items, setItems] = useState<Item[]>([]);
165170
const dispatch = useTypedDispatch();
166171

167172
function handleAddItem() {
173+
if (
174+
param?.schema?.maxItems !== undefined &&
175+
items.length >= param.schema.maxItems
176+
) {
177+
return;
178+
}
168179
setItems((i) => [
169180
...i,
170181
{
@@ -173,7 +184,7 @@ function ParamArrayFormItem({ param }: ParamProps) {
173184
]);
174185
}
175186

176-
useEffect(() => {
187+
function updateItems(items: Array<Item>) {
177188
const values = items
178189
.map((item) => item.value)
179190
.filter((item): item is string => !!item);
@@ -184,12 +195,13 @@ function ParamArrayFormItem({ param }: ParamProps) {
184195
value: values.length > 0 ? values : undefined,
185196
})
186197
);
187-
}, [dispatch, items, param]);
198+
}
188199

189200
function handleDeleteItem(itemToDelete: { id: string }) {
190201
return () => {
191202
const newItems = items.filter((i) => i.id !== itemToDelete.id);
192203
setItems(newItems);
204+
updateItems(newItems);
193205
};
194206
}
195207

@@ -202,6 +214,7 @@ function ParamArrayFormItem({ param }: ParamProps) {
202214
return i;
203215
});
204216
setItems(newItems);
217+
updateItems(newItems);
205218
};
206219
}
207220

@@ -230,7 +243,14 @@ function ParamArrayFormItem({ param }: ParamProps) {
230243
</button>
231244
</div>
232245
))}
233-
<button className={styles.buttonThin} onClick={handleAddItem}>
246+
<button
247+
className={styles.buttonThin}
248+
onClick={handleAddItem}
249+
disabled={
250+
param?.schema?.maxItems != null &&
251+
items.length >= param.schema.maxItems
252+
}
253+
>
234254
Add item
235255
</button>
236256
</>
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
/* ============================================================================
2+
* Copyright (c) Cloud Annotations
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
* ========================================================================== */
7+
8+
import sdk from "postman-collection";
9+
10+
import { openApiQueryParams2PostmanQueryParams } from "./buildPostmanRequest";
11+
12+
describe("openApiQueryParams2PostmanQueryParams", () => {
13+
it("should transform empty array to empty array", () => {
14+
const expected: sdk.QueryParam[] = [];
15+
const actual = openApiQueryParams2PostmanQueryParams([]);
16+
expect(actual).toStrictEqual(expected);
17+
});
18+
19+
it("default to comma delimited", () => {
20+
const expected: sdk.QueryParam[] = [
21+
new sdk.QueryParam({ key: "arrayParam", value: "abc,def" }),
22+
];
23+
const actual = openApiQueryParams2PostmanQueryParams([
24+
{
25+
name: "arrayParam",
26+
in: "query",
27+
value: ["abc", "def"],
28+
},
29+
]);
30+
expect(actual).toStrictEqual(expected);
31+
});
32+
33+
it("should expand params if explode=true", () => {
34+
const expected: sdk.QueryParam[] = [
35+
new sdk.QueryParam({ key: "arrayParam", value: "abc" }),
36+
new sdk.QueryParam({ key: "arrayParam", value: "def" }),
37+
];
38+
const actual = openApiQueryParams2PostmanQueryParams([
39+
{
40+
name: "arrayParam",
41+
in: "query",
42+
style: "form",
43+
explode: true,
44+
value: ["abc", "def"],
45+
},
46+
]);
47+
expect(actual).toStrictEqual(expected);
48+
});
49+
50+
it("should respect style=pipeDelimited", () => {
51+
const expected: sdk.QueryParam[] = [
52+
new sdk.QueryParam({ key: "arrayParam", value: "abc|def" }),
53+
];
54+
const actual = openApiQueryParams2PostmanQueryParams([
55+
{
56+
name: "arrayParam",
57+
in: "query",
58+
style: "pipeDelimited",
59+
value: ["abc", "def"],
60+
},
61+
]);
62+
expect(actual).toStrictEqual(expected);
63+
});
64+
65+
it("should respect style=spaceDelimited", () => {
66+
const expected: sdk.QueryParam[] = [
67+
new sdk.QueryParam({ key: "arrayParam", value: "abc%20def" }),
68+
];
69+
const actual = openApiQueryParams2PostmanQueryParams([
70+
{
71+
name: "arrayParam",
72+
in: "query",
73+
style: "spaceDelimited",
74+
value: ["abc", "def"],
75+
},
76+
]);
77+
expect(actual).toStrictEqual(expected);
78+
});
79+
});

packages/docusaurus-theme-openapi/src/theme/ApiDemoPanel/buildPostmanRequest.ts

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,19 +17,36 @@ type Param = {
1717
value?: string | string[];
1818
} & ParameterObject;
1919

20-
function setQueryParams(postman: sdk.Request, queryParams: Param[]) {
21-
postman.url.query.clear();
20+
export function openApiQueryParams2PostmanQueryParams(
21+
queryParams: Param[]
22+
): sdk.QueryParam[] {
23+
let qp = [];
24+
for (const param of queryParams) {
25+
if (Array.isArray(param.value) && param?.explode === true) {
26+
for (const value of param.value) {
27+
qp.push({ ...param, value });
28+
}
29+
} else {
30+
qp.push(param);
31+
}
32+
}
2233

23-
const qp = queryParams
34+
return qp
2435
.map((param) => {
2536
if (!param.value) {
2637
return undefined;
2738
}
2839

40+
let delimiter = ",";
41+
if (param?.style === "spaceDelimited") {
42+
delimiter = "%20";
43+
} else if (param?.style === "pipeDelimited") {
44+
delimiter = "|";
45+
}
2946
if (Array.isArray(param.value)) {
3047
return new sdk.QueryParam({
3148
key: param.name,
32-
value: param.value.map(encodeURIComponent).join(","),
49+
value: param.value.map(encodeURIComponent).join(delimiter),
3350
});
3451
}
3552

@@ -50,7 +67,11 @@ function setQueryParams(postman: sdk.Request, queryParams: Param[]) {
5067
});
5168
})
5269
.filter((item): item is sdk.QueryParam => item !== undefined);
70+
}
5371

72+
function setQueryParams(postman: sdk.Request, queryParams: Param[]) {
73+
postman.url.query.clear();
74+
const qp = openApiQueryParams2PostmanQueryParams(queryParams);
5475
if (qp.length > 0) {
5576
postman.addQueryParams(qp);
5677
}

0 commit comments

Comments
 (0)