From 8ca0447241d4f5895b0f803a7f83c09f5cc8a040 Mon Sep 17 00:00:00 2001 From: Gregory Schier Date: Wed, 19 Aug 2026 06:54:47 -0700 Subject: [PATCH] fix(openapi): serialize request parameters --- plugins/importer-openapi/src/index.ts | 169 ++++++++++++++++-- .../tests/__snapshots__/index.test.ts.snap | 8 +- plugins/importer-openapi/tests/index.test.ts | 120 +++++++++++++ 3 files changed, 279 insertions(+), 18 deletions(-) diff --git a/plugins/importer-openapi/src/index.ts b/plugins/importer-openapi/src/index.ts index c6ad51c0..49658049 100644 --- a/plugins/importer-openapi/src/index.ts +++ b/plugins/importer-openapi/src/index.ts @@ -598,17 +598,70 @@ function importUrlParameters({ .map((p) => importState.resolve(p)) .filter(isRecord) .filter((p) => stringAt(p, "in") === "query" || stringAt(p, "in") === "path") - .map((p) => ({ - enabled: p.required === true, - name: - stringAt(p, "in") === "path" - ? `:${stringAt(p, "name") ?? ""}` - : (stringAt(p, "name") ?? ""), - value: parameterExample(p, importState), - })) + .flatMap((p) => serializeUrlParameter(p, importState)) .filter(({ name }) => name.length > 0); } +function serializeUrlParameter( + parameter: UnknownRecord, + importState: ImportState, +): HttpUrlParameter[] { + const name = stringAt(parameter, "name") ?? ""; + const location = stringAt(parameter, "in"); + const enabled = parameter.required === true; + const value = parameterExampleValue(parameter, importState); + if (isRecord(parameter.content)) { + return [ + { + enabled, + name: location === "path" ? `:${name}` : name, + value: serializeContentParameter(parameter, importState), + }, + ]; + } + if (location === "path") { + return [{ enabled, name: `:${name}`, value: serializePathParameter(name, value, parameter) }]; + } + + if (isRecord(value)) { + const entries = Object.entries(value); + const style = stringAt(parameter, "style") ?? "form"; + const explode = parameter.explode !== false; + if (style === "deepObject") { + return entries.map(([key, entryValue]) => ({ + enabled, + name: `${name}[${key}]`, + value: stringifyExampleValue(entryValue), + })); + } + if (style === "form" && explode) { + return entries.map(([key, entryValue]) => ({ + enabled, + name: key, + value: stringifyExampleValue(entryValue), + })); + } + const separator = style === "spaceDelimited" ? " " : style === "pipeDelimited" ? "|" : ","; + return [{ enabled, name, value: entries.flat().map(stringifyExampleValue).join(separator) }]; + } + + if (Array.isArray(value)) { + const style = stringAt(parameter, "style") ?? "form"; + const explode = parameter.explode !== false; + if (style === "form" && explode) { + return value.map((entryValue) => ({ + enabled, + name, + value: stringifyExampleValue(entryValue), + })); + } + const separator = style === "spaceDelimited" ? " " : style === "pipeDelimited" ? "|" : ","; + return [{ enabled, name, value: value.map(stringifyExampleValue).join(separator) }]; + } + + return [{ enabled, name, value: stringifyExampleValue(value) }]; +} + function importHeaderParameters({ importState, parameters, @@ -620,18 +673,112 @@ function importHeaderParameters({ .map((p) => importState.resolve(p)) .filter(isRecord) .filter((p) => stringAt(p, "in") === "header") + .filter( + (p) => + !["accept", "authorization", "content-type"].includes( + (stringAt(p, "name") ?? "").toLowerCase(), + ), + ) .map((p) => ({ enabled: p.required === true, name: stringAt(p, "name") ?? "", - value: parameterExample(p, importState), + value: serializeParameterValue(p, importState), + })) + .filter(({ name }) => name.length > 0) + .concat(importCookieHeader(parameters, importState)); +} + +function importCookieHeader(parameters: unknown[], importState: ImportState): HttpRequestHeader[] { + const cookies = parameters + .map((p) => importState.resolve(p)) + .filter(isRecord) + .filter((p) => stringAt(p, "in") === "cookie") + .map((p) => ({ + enabled: p.required === true, + name: stringAt(p, "name") ?? "", + value: serializeParameterValue(p, importState), })) .filter(({ name }) => name.length > 0); + if (cookies.length === 0) return []; + return [ + { + enabled: cookies.some(({ enabled }) => enabled), + name: "Cookie", + value: cookies.map(({ name, value }) => `${name}=${value}`).join("; "), + }, + ]; +} + +function serializeParameterValue(parameter: UnknownRecord, importState: ImportState): string { + if (isRecord(parameter.content)) return serializeContentParameter(parameter, importState); + return serializeSimpleParameter(parameterExampleValue(parameter, importState), parameter); +} + +function serializeContentParameter(parameter: UnknownRecord, importState: ImportState): string { + const [contentType, rawMediaType] = Object.entries(toRecord(parameter.content))[0] ?? []; + const value = mediaTypeExample(toRecord(rawMediaType), importState); + return contentType?.toLowerCase().includes("json") + ? (JSON.stringify(value) ?? "") + : stringifyExampleValue(value); +} + +function serializePathParameter(name: string, value: unknown, parameter: UnknownRecord): string { + const style = stringAt(parameter, "style") ?? "simple"; + const explode = parameter.explode === true; + const values = Array.isArray(value) + ? value.map(stringifyExampleValue) + : isRecord(value) + ? Object.entries(value).flatMap(([key, entryValue]) => [ + key, + stringifyExampleValue(entryValue), + ]) + : [stringifyExampleValue(value)]; + + if (style === "label") { + if (explode && isRecord(value)) { + return `.${Object.entries(value) + .map(([key, entryValue]) => `${key}=${stringifyExampleValue(entryValue)}`) + .join(".")}`; + } + return `.${values.join(explode ? "." : ",")}`; + } + if (style === "matrix") { + if (explode && Array.isArray(value)) { + return value.map((entryValue) => `;${name}=${stringifyExampleValue(entryValue)}`).join(""); + } + if (explode && isRecord(value)) { + return Object.entries(value) + .map(([key, entryValue]) => `;${key}=${stringifyExampleValue(entryValue)}`) + .join(""); + } + return `;${name}=${values.join(",")}`; + } + return serializeSimpleParameter(value, parameter); +} + +function serializeSimpleParameter(value: unknown, parameter: UnknownRecord): string { + if (Array.isArray(value)) return value.map(stringifyExampleValue).join(","); + if (isRecord(value)) { + const entries = Object.entries(value); + return parameter.explode === true + ? entries.map(([key, entryValue]) => `${key}=${stringifyExampleValue(entryValue)}`).join(",") + : entries.flat().map(stringifyExampleValue).join(","); + } + return stringifyExampleValue(value); } function parameterExample(parameter: UnknownRecord, importState: ImportState): string { + return stringifyExampleValue(parameterExampleValue(parameter, importState)); +} + +function parameterExampleValue(parameter: UnknownRecord, importState: ImportState): unknown { const directExample = firstPresent(parameter.example, firstExampleValue(parameter.examples)); - if (directExample != null) return stringifyExampleValue(directExample); - return stringifyExampleValue(schemaToExample(importState.resolve(parameter.schema), importState)); + if (directExample != null) return directExample; + if (isRecord(parameter.content)) { + const mediaType = toRecord(Object.values(parameter.content)[0]); + return mediaTypeExample(mediaType, importState); + } + return schemaToExample(importState.resolve(parameter.schema), importState); } function importBody({ diff --git a/plugins/importer-openapi/tests/__snapshots__/index.test.ts.snap b/plugins/importer-openapi/tests/__snapshots__/index.test.ts.snap index 10458055..837b3ea3 100644 --- a/plugins/importer-openapi/tests/__snapshots__/index.test.ts.snap +++ b/plugins/importer-openapi/tests/__snapshots__/index.test.ts.snap @@ -840,13 +840,7 @@ Responses: - 200: Sucessful authentication. - 401: Unsuccessful authentication.", "folderId": "GENERATE_ID::FOLDER_1", - "headers": [ - { - "enabled": false, - "name": "Authorization", - "value": "", - }, - ], + "headers": [], "id": "GENERATE_ID::HTTP_REQUEST_15", "method": "GET", "model": "http_request", diff --git a/plugins/importer-openapi/tests/index.test.ts b/plugins/importer-openapi/tests/index.test.ts index da2c688c..35250182 100644 --- a/plugins/importer-openapi/tests/index.test.ts +++ b/plugins/importer-openapi/tests/index.test.ts @@ -524,6 +524,126 @@ describe("importer-openapi", () => { ]); }); + test("Imports cookie and content-based parameters", async () => { + const imported = await convertOpenApi( + JSON.stringify({ + openapi: "3.0.4", + info: { title: "Parameter Test", version: "1.0.0" }, + paths: { + "/items": { + get: { + parameters: [ + { + name: "session", + in: "cookie", + required: true, + schema: { type: "string", example: "abc" }, + }, + { + name: "X-Filter", + in: "header", + required: true, + content: { "text/plain": { example: "active" } }, + }, + ], + responses: {}, + }, + }, + }, + }), + ); + + expect(imported?.resources.httpRequests[0]?.headers).toEqual([ + { enabled: true, name: "X-Filter", value: "active" }, + { enabled: true, name: "Cookie", value: "session=abc" }, + ]); + }); + + test("Serializes structured query parameters according to style and explode", async () => { + const imported = await convertOpenApi( + JSON.stringify({ + openapi: "3.0.4", + info: { title: "Serialization Test", version: "1.0.0" }, + paths: { + "/items": { + get: { + parameters: [ + { + name: "filter", + in: "query", + required: true, + style: "deepObject", + explode: true, + schema: { + type: "object", + properties: { + role: { example: "admin" }, + active: { example: true }, + }, + }, + }, + { + name: "tags", + in: "query", + style: "form", + explode: true, + schema: { type: "array", example: ["one", "two"] }, + }, + ], + responses: {}, + }, + }, + }, + }), + ); + + expect(imported?.resources.httpRequests[0]?.urlParameters).toEqual([ + { enabled: true, name: "filter[role]", value: "admin" }, + { enabled: true, name: "filter[active]", value: "true" }, + { enabled: false, name: "tags", value: "one" }, + { enabled: false, name: "tags", value: "two" }, + ]); + }); + + test("Serializes label and matrix path parameters", async () => { + const imported = await convertOpenApi( + JSON.stringify({ + openapi: "3.0.4", + info: { title: "Path Serialization Test", version: "1.0.0" }, + paths: { + "/labels/{labels}/matrix/{coordinates}": { + get: { + parameters: [ + { + name: "labels", + in: "path", + required: true, + style: "label", + explode: true, + schema: { type: "array", example: ["one", "two"] }, + }, + { + name: "coordinates", + in: "path", + required: true, + style: "matrix", + explode: true, + schema: { type: "object", example: { x: 1, y: 2 } }, + }, + ], + responses: {}, + }, + }, + }, + }), + ); + + expect(imported?.resources.httpRequests[0]?.urlParameters).toEqual([ + { enabled: true, name: ":labels", value: ".one.two" }, + { enabled: true, name: ":coordinates", value: ";x=1;y=2" }, + ]); + }); + test("Prefers operation-level consumes for Swagger bodies", async () => { const imported = await convertOpenApi( JSON.stringify({