diff --git a/demo/examples/openapi-array-query-params.yaml b/demo/examples/openapi-array-query-params.yaml new file mode 100644 index 00000000..189c551e --- /dev/null +++ b/demo/examples/openapi-array-query-params.yaml @@ -0,0 +1,43 @@ +openapi: 3.0.3 +info: + version: 1.0.0 + title: "" +servers: + - url: https://example.com +paths: + /Things: + get: + summary: View Things + parameters: + - name: "arrayParam" + in: "query" + required: false + description: "You can pass 0, 1 or 2 occurrences of this in the query string" + style: "form" + explode: true + schema: + type: "array" + maxItems: 2 + items: + type: "string" + responses: + "200": + description: OK + /Stuff: + get: + summary: View Stuff + parameters: + - name: "arrayParam" + in: "query" + required: false + description: "You can pass 0, 1 or 2 occurrences of this in the query string" + style: "pipeDelimited" + explode: false + schema: + type: "array" + maxItems: 2 + items: + type: "string" + responses: + "200": + description: OK diff --git a/packages/docusaurus-plugin-openapi/src/openapi/types.ts b/packages/docusaurus-plugin-openapi/src/openapi/types.ts index 748ca606..2ac2ad07 100644 --- a/packages/docusaurus-plugin-openapi/src/openapi/types.ts +++ b/packages/docusaurus-plugin-openapi/src/openapi/types.ts @@ -179,7 +179,7 @@ export interface ParameterObject { allowEmptyValue?: boolean; // style?: string; - explode?: string; + explode?: boolean; allowReserved?: boolean; schema?: SchemaObject; example?: any; diff --git a/packages/docusaurus-theme-openapi/src/theme/ApiDemoPanel/ParamOptions/index.tsx b/packages/docusaurus-theme-openapi/src/theme/ApiDemoPanel/ParamOptions/index.tsx index 9099026d..10e2d2ba 100644 --- a/packages/docusaurus-theme-openapi/src/theme/ApiDemoPanel/ParamOptions/index.tsx +++ b/packages/docusaurus-theme-openapi/src/theme/ApiDemoPanel/ParamOptions/index.tsx @@ -5,7 +5,7 @@ * LICENSE file in the root directory of this source tree. * ========================================================================== */ -import React, { useState, useEffect } from "react"; +import React, { useState } from "react"; import { nanoid } from "@reduxjs/toolkit"; @@ -21,6 +21,11 @@ interface ParamProps { param: Param; } +interface Item { + id: string; + value?: string; +} + function ParamOption({ param }: ParamProps) { if (param.schema?.type === "array" && param.schema.items?.enum) { return ; @@ -161,10 +166,16 @@ function ArrayItem({ } function ParamArrayFormItem({ param }: ParamProps) { - const [items, setItems] = useState<{ id: string; value?: string }[]>([]); + const [items, setItems] = useState([]); const dispatch = useTypedDispatch(); function handleAddItem() { + if ( + param?.schema?.maxItems !== undefined && + items.length >= param.schema.maxItems + ) { + return; + } setItems((i) => [ ...i, { @@ -173,7 +184,7 @@ function ParamArrayFormItem({ param }: ParamProps) { ]); } - useEffect(() => { + function updateItems(items: Array) { const values = items .map((item) => item.value) .filter((item): item is string => !!item); @@ -184,12 +195,13 @@ function ParamArrayFormItem({ param }: ParamProps) { value: values.length > 0 ? values : undefined, }) ); - }, [dispatch, items, param]); + } function handleDeleteItem(itemToDelete: { id: string }) { return () => { const newItems = items.filter((i) => i.id !== itemToDelete.id); setItems(newItems); + updateItems(newItems); }; } @@ -202,6 +214,7 @@ function ParamArrayFormItem({ param }: ParamProps) { return i; }); setItems(newItems); + updateItems(newItems); }; } @@ -230,7 +243,14 @@ function ParamArrayFormItem({ param }: ParamProps) { ))} - diff --git a/packages/docusaurus-theme-openapi/src/theme/ApiDemoPanel/buildPostmanRequest.test.ts b/packages/docusaurus-theme-openapi/src/theme/ApiDemoPanel/buildPostmanRequest.test.ts new file mode 100644 index 00000000..a42bb0f5 --- /dev/null +++ b/packages/docusaurus-theme-openapi/src/theme/ApiDemoPanel/buildPostmanRequest.test.ts @@ -0,0 +1,79 @@ +/* ============================================================================ + * Copyright (c) Cloud Annotations + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * ========================================================================== */ + +import sdk from "postman-collection"; + +import { openApiQueryParams2PostmanQueryParams } from "./buildPostmanRequest"; + +describe("openApiQueryParams2PostmanQueryParams", () => { + it("should transform empty array to empty array", () => { + const expected: sdk.QueryParam[] = []; + const actual = openApiQueryParams2PostmanQueryParams([]); + expect(actual).toStrictEqual(expected); + }); + + it("default to comma delimited", () => { + const expected: sdk.QueryParam[] = [ + new sdk.QueryParam({ key: "arrayParam", value: "abc,def" }), + ]; + const actual = openApiQueryParams2PostmanQueryParams([ + { + name: "arrayParam", + in: "query", + value: ["abc", "def"], + }, + ]); + expect(actual).toStrictEqual(expected); + }); + + it("should expand params if explode=true", () => { + const expected: sdk.QueryParam[] = [ + new sdk.QueryParam({ key: "arrayParam", value: "abc" }), + new sdk.QueryParam({ key: "arrayParam", value: "def" }), + ]; + const actual = openApiQueryParams2PostmanQueryParams([ + { + name: "arrayParam", + in: "query", + style: "form", + explode: true, + value: ["abc", "def"], + }, + ]); + expect(actual).toStrictEqual(expected); + }); + + it("should respect style=pipeDelimited", () => { + const expected: sdk.QueryParam[] = [ + new sdk.QueryParam({ key: "arrayParam", value: "abc|def" }), + ]; + const actual = openApiQueryParams2PostmanQueryParams([ + { + name: "arrayParam", + in: "query", + style: "pipeDelimited", + value: ["abc", "def"], + }, + ]); + expect(actual).toStrictEqual(expected); + }); + + it("should respect style=spaceDelimited", () => { + const expected: sdk.QueryParam[] = [ + new sdk.QueryParam({ key: "arrayParam", value: "abc%20def" }), + ]; + const actual = openApiQueryParams2PostmanQueryParams([ + { + name: "arrayParam", + in: "query", + style: "spaceDelimited", + value: ["abc", "def"], + }, + ]); + expect(actual).toStrictEqual(expected); + }); +}); diff --git a/packages/docusaurus-theme-openapi/src/theme/ApiDemoPanel/buildPostmanRequest.ts b/packages/docusaurus-theme-openapi/src/theme/ApiDemoPanel/buildPostmanRequest.ts index f0e54cf9..85e5b596 100644 --- a/packages/docusaurus-theme-openapi/src/theme/ApiDemoPanel/buildPostmanRequest.ts +++ b/packages/docusaurus-theme-openapi/src/theme/ApiDemoPanel/buildPostmanRequest.ts @@ -17,19 +17,36 @@ type Param = { value?: string | string[]; } & ParameterObject; -function setQueryParams(postman: sdk.Request, queryParams: Param[]) { - postman.url.query.clear(); +export function openApiQueryParams2PostmanQueryParams( + queryParams: Param[] +): sdk.QueryParam[] { + let qp = []; + for (const param of queryParams) { + if (Array.isArray(param.value) && param?.explode === true) { + for (const value of param.value) { + qp.push({ ...param, value }); + } + } else { + qp.push(param); + } + } - const qp = queryParams + return qp .map((param) => { if (!param.value) { return undefined; } + let delimiter = ","; + if (param?.style === "spaceDelimited") { + delimiter = "%20"; + } else if (param?.style === "pipeDelimited") { + delimiter = "|"; + } if (Array.isArray(param.value)) { return new sdk.QueryParam({ key: param.name, - value: param.value.map(encodeURIComponent).join(","), + value: param.value.map(encodeURIComponent).join(delimiter), }); } @@ -50,7 +67,11 @@ function setQueryParams(postman: sdk.Request, queryParams: Param[]) { }); }) .filter((item): item is sdk.QueryParam => item !== undefined); +} +function setQueryParams(postman: sdk.Request, queryParams: Param[]) { + postman.url.query.clear(); + const qp = openApiQueryParams2PostmanQueryParams(queryParams); if (qp.length > 0) { postman.addQueryParams(qp); }