diff --git a/.changeset/big-lemons-wait.md b/.changeset/big-lemons-wait.md new file mode 100644 index 000000000..10f071b1c --- /dev/null +++ b/.changeset/big-lemons-wait.md @@ -0,0 +1,5 @@ +--- +'@gitbook/react-openapi': patch +--- + +Update OpenAPI code examples to support multiple content-type diff --git a/packages/react-openapi/src/OpenAPICodeSample.tsx b/packages/react-openapi/src/OpenAPICodeSample.tsx index bda4537b4..8b88f9e70 100644 --- a/packages/react-openapi/src/OpenAPICodeSample.tsx +++ b/packages/react-openapi/src/OpenAPICodeSample.tsx @@ -57,11 +57,7 @@ export function OpenAPICodeSample(props: { data.path + (searchParams.size ? `?${searchParams.toString()}` : ''), method: data.method, - body: requestBodyContent - ? generateMediaTypeExample(requestBodyContent[1], { - omitEmptyAndOptionalProperties: true, - }) - : undefined, + body: requestBodyContent ? generateMediaTypeExample(requestBodyContent[1]) : undefined, headers: { ...getSecurityHeaders(data.securities), ...headersObject, diff --git a/packages/react-openapi/src/OpenAPISchema.tsx b/packages/react-openapi/src/OpenAPISchema.tsx index 445a45c49..8c7c9bf5e 100644 --- a/packages/react-openapi/src/OpenAPISchema.tsx +++ b/packages/react-openapi/src/OpenAPISchema.tsx @@ -260,12 +260,7 @@ export function OpenAPISchemaPresentation(props: OpenAPISchemaPropertyEntry) { ) : null} {shouldDisplayExample(schema) ? (
- Example:{' '} - - {typeof schema.example === 'string' - ? schema.example - : stringifyOpenAPI(schema.example)} - + Example: {formatExample(schema.example)}
) : null} {schema.pattern ? ( @@ -434,3 +429,16 @@ function getDisclosureLabel(schema: OpenAPIV3.SchemaObject): string | undefined return schema.title; } + +function formatExample(example: any): string { + if (typeof example === 'string') { + return example + .replace(/\n/g, ' ') // Replace newlines with spaces + .replace(/\s+/g, ' ') // Collapse multiple spaces/newlines into a single space + .replace(/([\{\}:,])\s+/g, '$1 ') // Ensure a space after {, }, :, and , + .replace(/\s+([\{\}:,])/g, ' $1') // Ensure a space before {, }, :, and , + .trim(); + } + + return stringifyOpenAPI(example); +} diff --git a/packages/react-openapi/src/OpenAPITabs.tsx b/packages/react-openapi/src/OpenAPITabs.tsx index e2a5b3ebb..396b526a7 100644 --- a/packages/react-openapi/src/OpenAPITabs.tsx +++ b/packages/react-openapi/src/OpenAPITabs.tsx @@ -74,11 +74,17 @@ export function OpenAPITabs( const tabFromState = syncedTabs.get(stateKey); if (!items.some((item) => item.key === tabFromState?.key)) { - return; + return setSelectedTab(defaultTab); } if (tabFromState && tabFromState?.key !== selectedTab?.key) { - setSelectedTab(tabFromState); + const tabFromItems = items.find((item) => item.key === tabFromState.key); + + if (!tabFromItems) { + return; + } + + setSelectedTab(tabFromItems); } } }, [isVisible, stateKey, syncedTabs, selectedTabKey]); diff --git a/packages/react-openapi/src/code-samples.test.ts b/packages/react-openapi/src/code-samples.test.ts index 6d2aa1302..98e73a14d 100644 --- a/packages/react-openapi/src/code-samples.test.ts +++ b/packages/react-openapi/src/code-samples.test.ts @@ -1,5 +1,5 @@ -import { it, expect } from 'bun:test'; -import { parseHostAndPath } from './code-samples'; +import { it, expect, describe } from 'bun:test'; +import { codeSampleGenerators, CodeSampleInput, parseHostAndPath } from './code-samples'; it('should parse host and path on url strings properly', () => { const testUrls = [ @@ -49,3 +49,595 @@ it('should parse host and path on url strings properly', () => { }, ]); }); + +describe('curL code sample generator', () => { + const generator = codeSampleGenerators.find((g) => g.id === 'curl'); + + expect(generator).toBeDefined(); + + it('should format application/x-www-form-urlencoded body properly', () => { + const input: CodeSampleInput = { + method: 'GET', + url: 'https://example.com/path', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + }, + body: { + key: 'value', + }, + }; + + const output = generator!.generate(input); + + expect(output).toBe( + "curl -L \\\n --url 'https://example.com/path' \\\n --header 'Content-Type: application/x-www-form-urlencoded' \\\n --data 'key=value'", + ); + }); + + it('should format application/json body properly', () => { + const input: CodeSampleInput = { + method: 'GET', + url: 'https://example.com/path', + headers: { + 'Content-Type': 'application/json', + }, + body: { + key: 'value', + }, + }; + + const output = generator!.generate(input); + + expect(output).toBe( + "curl -L \\\n --url 'https://example.com/path' \\\n --header 'Content-Type: application/json' \\\n --data '{\n \"key\": \"value\"\n }'", + ); + }); + + it('should format application/xml body properly', () => { + const input: CodeSampleInput = { + method: 'GET', + url: 'https://example.com/path', + headers: { + 'Content-Type': 'application/xml', + }, + body: 'value', + }; + + const output = generator!.generate(input); + + expect(output).toBe( + "curl -L \\\n --url 'https://example.com/path' \\\n --header 'Content-Type: application/xml' \\\n --data-binary $'value'", + ); + }); + + it('should format application/graphql body properly', () => { + const input: CodeSampleInput = { + method: 'GET', + url: 'https://example.com/path', + headers: { + 'Content-Type': 'application/graphql', + }, + body: '{ key }', + }; + + const output = generator!.generate(input); + + expect(output).toBe( + "curl -L \\\n --url 'https://example.com/path' \\\n --header 'Content-Type: application/json' \\\n --data '\"{ key }\"'", + ); + }); + + it('should format text/csv body properly', () => { + const input: CodeSampleInput = { + method: 'GET', + url: 'https://example.com/path', + headers: { + 'Content-Type': 'text/csv', + }, + body: 'key,value', + }; + + const output = generator!.generate(input); + + expect(output).toBe( + "curl -L \\\n --url 'https://example.com/path' \\\n --header 'Content-Type: text/csv' \\\n --data-binary $'key,value'", + ); + }); + + it('should format application/pdf body properly', () => { + const input: CodeSampleInput = { + method: 'GET', + url: 'https://example.com/path', + headers: { + 'Content-Type': 'application/pdf', + }, + body: 'file', + }; + + const output = generator!.generate(input); + + expect(output).toBe( + "curl -L \\\n --url 'https://example.com/path' \\\n --header 'Content-Type: application/pdf' \\\n --data-binary '@file'", + ); + }); + + it('should format text/plain body properly', () => { + const input: CodeSampleInput = { + method: 'GET', + url: 'https://example.com/path', + headers: { + 'Content-Type': 'text/plain', + }, + body: 'value', + }; + + const output = generator!.generate(input); + + expect(output).toBe( + "curl -L \\\n --url 'https://example.com/path' \\\n --header 'Content-Type: text/plain' \\\n --data 'value'", + ); + }); + + it('should format multipart/form-data body properly', () => { + const input: CodeSampleInput = { + method: 'GET', + url: 'https://example.com/path', + headers: { + 'Content-Type': 'multipart/form-data', + }, + body: { + key: 'value', + }, + }; + + const output = generator!.generate(input); + + expect(output).toBe( + "curl -L \\\n --url 'https://example.com/path' \\\n --header 'Content-Type: multipart/form-data' \\\n --form 'key=value'", + ); + }); +}); + +describe('javascript code sample generator', () => { + const generator = codeSampleGenerators.find((g) => g.id === 'javascript'); + + expect(generator).toBeDefined(); + + it('should format application/x-www-form-urlencoded body properly', () => { + const input: CodeSampleInput = { + method: 'GET', + url: 'https://example.com/path', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + }, + body: { + key: 'value', + }, + }; + + const output = generator!.generate(input); + + expect(output).toBe( + 'const params = new URLSearchParams();\n\nparams.append("key", "value");\n\nconst response = await fetch(\'https://example.com/path\', {\n method: \'GET\',\n headers: {\n "Content-Type": "application/x-www-form-urlencoded"\n },\n body: params.toString()\n});\n\nconst data = await response.json();', + ); + }); + + it('should format application/json body properly', () => { + const input: CodeSampleInput = { + method: 'GET', + url: 'https://example.com/path', + headers: { + 'Content-Type': 'application/json', + }, + body: { + key: 'value', + }, + }; + + const output = generator!.generate(input); + + expect(output).toBe( + 'const response = await fetch(\'https://example.com/path\', {\n method: \'GET\',\n headers: {\n "Content-Type": "application/json"\n },\n body: JSON.stringify({\n "key": "value"\n })\n});\n\nconst data = await response.json();', + ); + }); + + it('should format application/xml body properly', () => { + const input: CodeSampleInput = { + method: 'GET', + url: 'https://example.com/path', + headers: { + 'Content-Type': 'application/xml', + }, + body: 'value', + }; + + const output = generator!.generate(input); + + expect(output).toBe( + 'const xml = `\n value`;\n\nconst response = await fetch(\'https://example.com/path\', {\n method: \'GET\',\n headers: {\n "Content-Type": "application/xml"\n },\n body: xml\n});\n\nconst data = await response.json();', + ); + }); + + it('should format application/graphql body properly', () => { + const input: CodeSampleInput = { + method: 'GET', + url: 'https://example.com/path', + headers: { + 'Content-Type': 'application/graphql', + }, + body: '{ key }', + }; + + const output = generator!.generate(input); + + expect(output).toBe( + 'const query = `\n { key }`;\n\nconst response = await fetch(\'https://example.com/path\', {\n method: \'GET\',\n headers: {\n "Content-Type": "application/graphql"\n },\n body: JSON.stringify(query)\n});\n\nconst data = await response.json();', + ); + }); + + it('should format text/csv body properly', () => { + const input: CodeSampleInput = { + method: 'GET', + url: 'https://example.com/path', + headers: { + 'Content-Type': 'text/csv', + }, + body: 'key,value', + }; + + const output = generator!.generate(input); + + expect(output).toBe( + 'const csv = `\n key,value`;\n\nconst response = await fetch(\'https://example.com/path\', {\n method: \'GET\',\n headers: {\n "Content-Type": "text/csv"\n },\n body: csv\n});\n\nconst data = await response.json();', + ); + }); + + it('should format application/pdf body properly', () => { + const input: CodeSampleInput = { + method: 'GET', + url: 'https://example.com/path', + headers: { + 'Content-Type': 'application/pdf', + }, + body: 'file', + }; + + const output = generator!.generate(input); + + expect(output).toBe( + 'const formData = new FormData();\n\nformData.append("file", "file");\n\nconst response = await fetch(\'https://example.com/path\', {\n method: \'GET\',\n headers: {\n "Content-Type": "application/pdf"\n },\n body: formData\n});\n\nconst data = await response.json();', + ); + }); + + it('should format text/plain body properly', () => { + const input: CodeSampleInput = { + method: 'GET', + url: 'https://example.com/path', + headers: { + 'Content-Type': 'text/plain', + }, + body: 'value', + }; + + const output = generator!.generate(input); + + expect(output).toBe( + 'const response = await fetch(\'https://example.com/path\', {\n method: \'GET\',\n headers: {\n "Content-Type": "text/plain"\n },\n body: "value"\n});\n\nconst data = await response.json();', + ); + }); + + it('should format multipart/form-data body properly', () => { + const input: CodeSampleInput = { + method: 'GET', + url: 'https://example.com/path', + headers: { + 'Content-Type': 'multipart/form-data', + }, + body: { + key: 'value', + }, + }; + + const output = generator!.generate(input); + + expect(output).toBe( + 'const formData = new FormData();\n\nformData.append("key", "value");\n\nconst response = await fetch(\'https://example.com/path\', {\n method: \'GET\',\n headers: {\n "Content-Type": "multipart/form-data"\n },\n body: formData\n});\n\nconst data = await response.json();', + ); + }); +}); + +describe('python code sample generator', () => { + const generator = codeSampleGenerators.find((g) => g.id === 'python'); + + expect(generator).toBeDefined(); + + it('should format application/x-www-form-urlencoded body properly', () => { + const input: CodeSampleInput = { + method: 'GET', + url: 'https://example.com/path', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + }, + body: { + key: 'value', + }, + }; + + const output = generator!.generate(input); + + expect(output).toBe( + 'import requests\n\nresponse = requests.get(\n "https://example.com/path",\n headers={"Content-Type":"application/x-www-form-urlencoded"},\n data={"key":"value"}\n)\n\ndata = response.json()', + ); + }); + + it('should format application/json body properly', () => { + const input: CodeSampleInput = { + method: 'GET', + url: 'https://example.com/path', + headers: { + 'Content-Type': 'application/json', + }, + body: { + key: 'value', + }, + }; + + const output = generator!.generate(input); + + expect(output).toBe( + 'import requests\n\nresponse = requests.get(\n "https://example.com/path",\n headers={"Content-Type":"application/json"},\n data={"key":"value"}\n)\n\ndata = response.json()', + ); + }); + + it('should format application/xml body properly', () => { + const input: CodeSampleInput = { + method: 'GET', + url: 'https://example.com/path', + headers: { + 'Content-Type': 'application/xml', + }, + body: 'value', + }; + + const output = generator!.generate(input); + + expect(output).toBe( + 'import requests\n\nresponse = requests.get(\n "https://example.com/path",\n headers={"Content-Type":"application/xml"},\n data="value"\n)\n\ndata = response.json()', + ); + }); + + it('should format application/graphql body properly', () => { + const input: CodeSampleInput = { + method: 'GET', + url: 'https://example.com/path', + headers: { + 'Content-Type': 'application/graphql', + }, + body: '{ key }', + }; + + const output = generator!.generate(input); + + expect(output).toBe( + 'import requests\n\nresponse = requests.get(\n "https://example.com/path",\n headers={"Content-Type":"application/graphql"},\n data="{ key }"\n)\n\ndata = response.json()', + ); + }); + + it('should format text/csv body properly', () => { + const input: CodeSampleInput = { + method: 'GET', + url: 'https://example.com/path', + headers: { + 'Content-Type': 'text/csv', + }, + body: 'key,value', + }; + + const output = generator!.generate(input); + + expect(output).toBe( + 'import requests\n\nresponse = requests.get(\n "https://example.com/path",\n headers={"Content-Type":"text/csv"},\n data="key,value"\n)\n\ndata = response.json()', + ); + }); + + it('should format application/pdf body properly', () => { + const input: CodeSampleInput = { + method: 'GET', + url: 'https://example.com/path', + headers: { + 'Content-Type': 'application/pdf', + }, + body: 'file', + }; + + const output = generator!.generate(input); + + expect(output).toBe( + 'import requests\n\nfiles = {\n "file": "file",\n}\n\nresponse = requests.get(\n "https://example.com/path",\n headers={"Content-Type":"application/pdf"},\n files=files\n)\n\ndata = response.json()', + ); + }); + + it('should format text/plain body properly', () => { + const input: CodeSampleInput = { + method: 'GET', + url: 'https://example.com/path', + headers: { + 'Content-Type': 'text/plain', + }, + body: 'value', + }; + + const output = generator!.generate(input); + + expect(output).toBe( + 'import requests\n\nresponse = requests.get(\n "https://example.com/path",\n headers={"Content-Type":"text/plain"},\n data="value"\n)\n\ndata = response.json()', + ); + }); + + it('should format multipart/form-data body properly', () => { + const input: CodeSampleInput = { + method: 'GET', + url: 'https://example.com/path', + headers: { + 'Content-Type': 'multipart/form-data', + }, + body: { + key: 'value', + }, + }; + + const output = generator!.generate(input); + + expect(output).toBe( + 'import requests\n\nfiles = {\n "key": "value",\n}\n\nresponse = requests.get(\n "https://example.com/path",\n headers={"Content-Type":"multipart/form-data"},\n files=files\n)\n\ndata = response.json()', + ); + }); +}); + +describe('http code sample generator', () => { + const generator = codeSampleGenerators.find((g) => g.id === 'http'); + + expect(generator).toBeDefined(); + + it('should format application/x-www-form-urlencoded body properly', () => { + const input: CodeSampleInput = { + method: 'GET', + url: 'https://example.com/path', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + }, + body: { + key: 'value', + }, + }; + + const output = generator!.generate(input); + + expect(output).toBe( + 'GET /path HTTP/1.1\nHost: example.com\nContent-Type: application/x-www-form-urlencoded\nContent-Length: 15\nAccept: */*\n\n"key=value"', + ); + }); + + it('should format application/json body properly', () => { + const input: CodeSampleInput = { + method: 'GET', + url: 'https://example.com/path', + headers: { + 'Content-Type': 'application/json', + }, + body: { + key: 'value', + }, + }; + + const output = generator!.generate(input); + + expect(output).toBe( + 'GET /path HTTP/1.1\nHost: example.com\nContent-Type: application/json\nContent-Length: 15\nAccept: */*\n\n{\n "key": "value"\n}', + ); + }); + + it('should format application/xml body properly', () => { + const input: CodeSampleInput = { + method: 'GET', + url: 'https://example.com/path', + headers: { + 'Content-Type': 'application/xml', + }, + body: 'value', + }; + + const output = generator!.generate(input); + + expect(output).toBe( + 'GET /path HTTP/1.1\nHost: example.com\nContent-Type: application/xml\nContent-Length: 18\nAccept: */*\n\n"value"', + ); + }); + + it('should format application/graphql body properly', () => { + const input: CodeSampleInput = { + method: 'GET', + url: 'https://example.com/path', + headers: { + 'Content-Type': 'application/graphql', + }, + body: '{ key }', + }; + + const output = generator!.generate(input); + + expect(output).toBe( + 'GET /path HTTP/1.1\nHost: example.com\nContent-Type: application/graphql\nContent-Length: 9\nAccept: */*\n\n"{ key }"', + ); + }); + + it('should format text/csv body properly', () => { + const input: CodeSampleInput = { + method: 'GET', + url: 'https://example.com/path', + headers: { + 'Content-Type': 'text/csv', + }, + body: 'key,value', + }; + + const output = generator!.generate(input); + + expect(output).toBe( + 'GET /path HTTP/1.1\nHost: example.com\nContent-Type: text/csv\nContent-Length: 11\nAccept: */*\n\n"key,value"', + ); + }); + + it('should format application/pdf body properly', () => { + const input: CodeSampleInput = { + method: 'GET', + url: 'https://example.com/path', + headers: { + 'Content-Type': 'application/pdf', + }, + body: 'file', + }; + + const output = generator!.generate(input); + + expect(output).toBe( + 'GET /path HTTP/1.1\nHost: example.com\nContent-Type: application/pdf\nContent-Length: 6\nAccept: */*\n\n"file"', + ); + }); + + it('should format text/plain body properly', () => { + const input: CodeSampleInput = { + method: 'GET', + url: 'https://example.com/path', + headers: { + 'Content-Type': 'text/plain', + }, + body: 'value', + }; + + const output = generator!.generate(input); + + expect(output).toBe( + 'GET /path HTTP/1.1\nHost: example.com\nContent-Type: text/plain\nContent-Length: 7\nAccept: */*\n\n"value"', + ); + }); + + it('should format multipart/form-data body properly', () => { + const input: CodeSampleInput = { + method: 'GET', + url: 'https://example.com/path', + headers: { + 'Content-Type': 'multipart/form-data', + }, + body: { + key: 'value', + }, + }; + + const output = generator!.generate(input); + + expect(output).toBe( + 'GET /path HTTP/1.1\nHost: example.com\nContent-Type: multipart/form-data\nContent-Length: 15\nAccept: */*\n\n{\n "key": "value"\n}', + ); + }); +}); diff --git a/packages/react-openapi/src/code-samples.ts b/packages/react-openapi/src/code-samples.ts index 4f5339e13..15187621b 100644 --- a/packages/react-openapi/src/code-samples.ts +++ b/packages/react-openapi/src/code-samples.ts @@ -1,3 +1,13 @@ +import { + isFormData, + isPDF, + isFormUrlEncoded, + isText, + isXML, + isCSV, + isGraphQL, + isPlainObject, +} from './contentTypeChecks'; import { stringifyOpenAPI } from './stringifyOpenAPI'; export interface CodeSampleInput { @@ -30,14 +40,27 @@ export const codeSampleGenerators: CodeSampleGenerator[] = [ lines.push(`--url '${url}'`); - if (headers) { + if (body) { + const bodyContent = BodyGenerators.getCurlBody(body, headers); + + if (bodyContent) { + body = bodyContent.body; + headers = bodyContent.headers; + } + } + + if (headers && Object.keys(headers).length > 0) { Object.entries(headers).forEach(([key, value]) => { lines.push(`--header '${key}: ${value}'`); }); } - if (body && Object.keys(body).length > 0) { - lines.push(`--data '${stringifyOpenAPI(body)}'`); + if (body) { + if (Array.isArray(body)) { + lines.push(...body); + } else { + lines.push(body); + } } return lines.map((line, index) => (index > 0 ? indent(line, 2) : line)).join(separator); @@ -50,18 +73,29 @@ export const codeSampleGenerators: CodeSampleGenerator[] = [ generate: ({ method, url, headers, body }) => { let code = ''; + if (body) { + const lines = BodyGenerators.getJavaScriptBody(body, headers); + + if (lines) { + // add the generated code to the top + code += lines.code; + body = lines.body; + headers = lines.headers; + } + } + code += `const response = await fetch('${url}', { method: '${method.toUpperCase()}',\n`; - if (headers) { + if (headers && Object.keys(headers).length > 0) { code += indent(`headers: ${stringifyOpenAPI(headers, null, 2)},\n`, 4); } if (body) { - code += indent(`body: JSON.stringify(${stringifyOpenAPI(body, null, 2)}),\n`, 4); + code += indent(`body: ${body}\n`, 4); } - code += `});\n`; + code += `});\n\n`; code += `const data = await response.json();`; return code; @@ -73,15 +107,34 @@ export const codeSampleGenerators: CodeSampleGenerator[] = [ syntax: 'python', generate: ({ method, url, headers, body }) => { let code = 'import requests\n\n'; + + if (body) { + const lines = BodyGenerators.getPythonBody(body, headers); + + // add the generated code to the top + if (lines) { + code += lines.code; + body = lines.body; + headers = lines.headers; + } + } + code += `response = requests.${method.toLowerCase()}(\n`; code += indent(`"${url}",\n`, 4); - if (headers) { + + if (headers && Object.keys(headers).length > 0) { code += indent(`headers=${stringifyOpenAPI(headers)},\n`, 4); } + if (body) { - code += indent(`json=${stringifyOpenAPI(body)}\n`, 4); + if (body === 'files') { + code += indent(`files=${body}\n`, 4); + } else { + code += indent(`data=${stringifyOpenAPI(body)}\n`, 4); + } } - code += ')\n'; + + code += ')\n\n'; code += `data = response.json()`; return code; }, @@ -99,6 +152,12 @@ export const codeSampleGenerators: CodeSampleGenerator[] = [ // handle unicode chars with a text encoder const encoder = new TextEncoder(); + const bodyString = BodyGenerators.getHTTPBody(body, headers); + + if (bodyString) { + body = bodyString; + } + headers = { ...headers, 'Content-Length': encoder.encode(bodyContent).length.toString(), @@ -117,7 +176,7 @@ export const codeSampleGenerators: CodeSampleGenerator[] = [ .join('\n') + '\n' : ''; - const bodyString = body ? `\n${stringifyOpenAPI(body, null, 2)}` : ''; + const bodyString = body ? `\n${body}` : ''; const httpRequest = `${method.toUpperCase()} ${decodeURI(path)} HTTP/1.1 Host: ${host} @@ -157,3 +216,165 @@ export function parseHostAndPath(url: string) { return { host, path }; } } + +// Body Generators +const BodyGenerators = { + getCurlBody(body: any, headers?: Record) { + if (!body || !headers) return undefined; + + // Copy headers to avoid mutating the original object + const headersCopy = { ...headers }; + const contentType: string = headersCopy['Content-Type'] || ''; + + if (isFormData(contentType)) { + body = isPlainObject(body) + ? Object.entries(body).map(([key, value]) => `--form '${key}=${String(value)}'`) + : `--form 'file=@${body}'`; + } else if (isFormUrlEncoded(contentType)) { + body = isPlainObject(body) + ? `--data '${Object.entries(body) + .map(([key, value]) => `${key}=${String(value)}`) + .join('&')}'` + : String(body); + } else if (isText(contentType)) { + body = `--data '${String(body).replace(/"/g, '')}'`; + } else if (isXML(contentType) || isCSV(contentType)) { + // We use --data-binary to avoid cURL converting newlines to \r\n + body = `--data-binary $'${stringifyOpenAPI(body).replace(/"/g, '')}'`; + } else if (isGraphQL(contentType)) { + body = `--data '${stringifyOpenAPI(body)}'`; + // Set Content-Type to application/json for GraphQL, recommended by GraphQL spec + headersCopy['Content-Type'] = 'application/json'; + } else if (isPDF(contentType)) { + // We use --data-binary to avoid cURL converting newlines to \r\n + body = `--data-binary '@${String(body)}'`; + } else { + body = `--data '${stringifyOpenAPI(body, null, 2)}'`; + } + + return { + body, + headers: headersCopy, + }; + }, + getJavaScriptBody: (body: any, headers?: Record) => { + if (!body || !headers) return; + + let code = ''; + + // Copy headers to avoid mutating the original object + const headersCopy = { ...headers }; + const contentType: string = headersCopy['Content-Type'] || ''; + + // Use FormData for file uploads + if (isFormData(contentType)) { + code += 'const formData = new FormData();\n\n'; + if (isPlainObject(body)) { + Object.entries(body).forEach(([key, value]) => { + code += `formData.append("${key}", "${String(value)}");\n`; + }); + } else if (typeof body === 'string') { + code += `formData.append("file", "${body}");\n`; + } + code += '\n'; + body = 'formData'; + } else if (isFormUrlEncoded(contentType)) { + // Use URLSearchParams for form-urlencoded data + code += 'const params = new URLSearchParams();\n\n'; + if (isPlainObject(body)) { + Object.entries(body).forEach(([key, value]) => { + code += `params.append("${key}", "${String(value)}");\n`; + }); + } + code += '\n'; + body = 'params.toString()'; + } else if (isGraphQL(contentType)) { + if (isPlainObject(body)) { + Object.entries(body).forEach(([key, value]) => { + code += `const ${key} = \`\n${indent(String(value), 4)}\`;\n\n`; + }); + body = `JSON.stringify({ ${Object.keys(body).join(', ')} })`; + // Set Content-Type to application/json for GraphQL, recommended by GraphQL spec + headersCopy['Content-Type'] = 'application/json'; + } else { + code += `const query = \`\n${indent(String(body), 4)}\`;\n\n`; + body = 'JSON.stringify(query)'; + } + } else if (isCSV(contentType)) { + code += 'const csv = `\n'; + code += indent(String(body), 4); + code += '`;\n\n'; + body = 'csv'; + } else if (isPDF(contentType)) { + // Use FormData to upload PDF files + code += 'const formData = new FormData();\n\n'; + code += `formData.append("file", "${body}");\n\n`; + body = 'formData'; + } else if (isXML(contentType)) { + code += 'const xml = `\n'; + code += indent(String(body), 4); + code += '`;\n\n'; + body = 'xml'; + } else if (isText(contentType)) { + body = stringifyOpenAPI(body, null, 2); + } else { + body = `JSON.stringify(${stringifyOpenAPI(body, null, 2)})`; + } + + return { body, code, headers: headersCopy }; + }, + getPythonBody: (body: any, headers?: Record) => { + if (!body || !headers) return; + let code = ''; + const contentType: string = headers['Content-Type'] || ''; + + if (isFormData(contentType)) { + code += 'files = {\n'; + if (isPlainObject(body)) { + Object.entries(body).forEach(([key, value]) => { + code += indent(`"${key}": "${String(value)}",`, 4) + '\n'; + }); + } + code += '}\n\n'; + body = 'files'; + } + + if (isPDF(contentType)) { + code += 'files = {\n'; + code += indent(`"file": "${body}",`, 4) + '\n'; + code += '}\n\n'; + body = 'files'; + } + + return { body, code, headers }; + }, + getHTTPBody: (body: any, headers?: Record) => { + if (!body || !headers) return undefined; + + const contentType: string = headers['Content-Type'] || ''; + + const typeHandlers = { + pdf: () => `${stringifyOpenAPI(body, null, 2)}`, + formUrlEncoded: () => { + const encoded = isPlainObject(body) + ? Object.entries(body) + .map(([key, value]) => `${key}=${String(value)}`) + .join('&') + : String(body); + return `"${encoded}"`; + }, + text: () => `"${String(body)}"`, + xmlOrCsv: () => `"${stringifyOpenAPI(body).replace(/"/g, '')}"`, + default: () => `${stringifyOpenAPI(body, null, 2)}`, + }; + + if (isPDF(contentType)) return typeHandlers.pdf(); + if (isFormUrlEncoded(contentType)) return typeHandlers.formUrlEncoded(); + if (isText(contentType)) return typeHandlers.text(); + if (isXML(contentType) || isCSV(contentType)) { + return typeHandlers.xmlOrCsv(); + } + + return typeHandlers.default(); + }, +}; diff --git a/packages/react-openapi/src/contentTypeChecks.ts b/packages/react-openapi/src/contentTypeChecks.ts new file mode 100644 index 000000000..06f106f42 --- /dev/null +++ b/packages/react-openapi/src/contentTypeChecks.ts @@ -0,0 +1,35 @@ +export function isJSON(contentType?: string): boolean { + return contentType?.toLowerCase().includes('application/json') || false; +} + +export function isXML(contentType?: string): boolean { + return contentType?.toLowerCase().includes('application/xml') || false; +} + +export function isGraphQL(contentType?: string): boolean { + return contentType?.toLowerCase().includes('application/graphql') || false; +} + +export function isCSV(contentType?: string): boolean { + return contentType?.toLowerCase().includes('text/csv') || false; +} + +export function isPDF(contentType?: string): boolean { + return contentType?.toLowerCase().includes('application/pdf') || false; +} + +export function isText(contentType?: string): boolean { + return contentType?.toLowerCase().includes('text/plain') || false; +} + +export function isFormUrlEncoded(contentType?: string): boolean { + return contentType?.toLowerCase().includes('application/x-www-form-urlencoded') || false; +} + +export function isFormData(contentType?: string): boolean { + return !!contentType && contentType.toLowerCase().includes('multipart/form-data'); +} + +export function isPlainObject(value: unknown): boolean { + return typeof value === 'object' && value !== null && !Array.isArray(value); +}