From ae78fc539487414282dc9ab417a12dcc7a529403 Mon Sep 17 00:00:00 2001 From: "Nolann B." <100787331+nolannbiron@users.noreply.github.com> Date: Tue, 25 Mar 2025 15:11:45 +0100 Subject: [PATCH] Fix XML in code sample (#3030) --- .changeset/early-singers-train.md | 5 ++ .../react-openapi/src/code-samples.test.ts | 70 ++++++++++++++++++- packages/react-openapi/src/code-samples.ts | 54 +++++++++++--- 3 files changed, 119 insertions(+), 10 deletions(-) create mode 100644 .changeset/early-singers-train.md diff --git a/.changeset/early-singers-train.md b/.changeset/early-singers-train.md new file mode 100644 index 000000000..edec2637e --- /dev/null +++ b/.changeset/early-singers-train.md @@ -0,0 +1,5 @@ +--- +'@gitbook/react-openapi': patch +--- + +Fix XML in code sample diff --git a/packages/react-openapi/src/code-samples.test.ts b/packages/react-openapi/src/code-samples.test.ts index 4945f9fc9..adcb3922a 100644 --- a/packages/react-openapi/src/code-samples.test.ts +++ b/packages/react-openapi/src/code-samples.test.ts @@ -110,6 +110,23 @@ describe('curL code sample generator', () => { ); }); + it('should convert json to xml body properly', () => { + const input: CodeSampleInput = { + method: 'GET', + url: 'https://example.com/path', + headers: { + 'Content-Type': 'application/xml', + }, + body: '{ "key": "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 $'\n value\n '" + ); + }); + it('should format application/graphql body properly', () => { const input: CodeSampleInput = { method: 'GET', @@ -258,6 +275,23 @@ describe('javascript code sample generator', () => { ); }); + it('should convert json to xml body properly', () => { + const input: CodeSampleInput = { + method: 'GET', + url: 'https://example.com/path', + headers: { + 'Content-Type': 'application/xml', + }, + body: '{ "key": "value" }', + }; + + const output = generator?.generate(input); + + expect(output).toBe( + 'const xml = `\n \n value\n`;\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', @@ -406,6 +440,23 @@ describe('python code sample generator', () => { ); }); + it('should convert json to xml body properly', () => { + const input: CodeSampleInput = { + method: 'GET', + url: 'https://example.com/path', + headers: { + 'Content-Type': 'application/xml', + }, + 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/xml"},\n data="\\nvalue\\n"\n)\n\ndata = response.json()' + ); + }); + it('should format application/graphql body properly', () => { const input: CodeSampleInput = { method: 'GET', @@ -514,7 +565,7 @@ describe('http code sample generator', () => { 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"' + 'GET /path HTTP/1.1\nHost: example.com\nContent-Type: application/x-www-form-urlencoded\nContent-Length: 15\nAccept: */*\n\n"key=\'value\'"' ); }); @@ -554,6 +605,23 @@ describe('http code sample generator', () => { ); }); + it('should convert json to xml body properly', () => { + const input: CodeSampleInput = { + method: 'GET', + url: 'https://example.com/path', + headers: { + 'Content-Type': 'application/xml', + }, + body: '{ "key": "value" }', + }; + + const output = generator?.generate(input); + + expect(output).toBe( + 'GET /path HTTP/1.1\nHost: example.com\nContent-Type: application/xml\nContent-Length: 24\nAccept: */*\n\n"\nvalue\n"' + ); + }); + it('should format application/graphql body properly', () => { const input: CodeSampleInput = { method: 'GET', diff --git a/packages/react-openapi/src/code-samples.ts b/packages/react-openapi/src/code-samples.ts index 24bd1ca72..44d29b95b 100644 --- a/packages/react-openapi/src/code-samples.ts +++ b/packages/react-openapi/src/code-samples.ts @@ -8,6 +8,7 @@ import { isText, isXML, } from './contentTypeChecks'; +import { json2xml } from './json2xml'; import { stringifyOpenAPI } from './stringifyOpenAPI'; export interface CodeSampleInput { @@ -238,7 +239,10 @@ const BodyGenerators = { : String(body); } else if (isText(contentType)) { body = `--data '${String(body).replace(/"/g, '')}'`; - } else if (isXML(contentType) || isCSV(contentType)) { + } else if (isXML(contentType)) { + // Convert to XML and ensure proper formatting + body = `--data-binary $'${convertBodyToXML(body)}'`; + } else if (isCSV(contentType)) { // We use --data-binary to avoid cURL converting newlines to \r\n body = `--data-binary $'${stringifyOpenAPI(body).replace(/"/g, '').replace(/\\n/g, '\n')}'`; } else if (isGraphQL(contentType)) { @@ -312,7 +316,9 @@ const BodyGenerators = { body = 'formData'; } else if (isXML(contentType)) { code += 'const xml = `\n'; - code += indent(String(body), 4); + + // Convert JSON to XML if needed + code += indent(convertBodyToXML(body), 4); code += '`;\n\n'; body = 'xml'; } else if (isText(contentType)) { @@ -346,6 +352,11 @@ const BodyGenerators = { body = 'files'; } + if (isXML(contentType)) { + // Convert JSON to XML if needed + body = convertBodyToXML(body); + } + return { body, code, headers }; }, getHTTPBody: (body: any, headers?: Record) => { @@ -358,23 +369,48 @@ const BodyGenerators = { formUrlEncoded: () => { const encoded = isPlainObject(body) ? Object.entries(body) - .map(([key, value]) => `${key}=${String(value)}`) + .map(([key, value]) => `${key}=${stringifyOpenAPI(value)}`) .join('&') - : String(body); - return `"${encoded}"`; + : stringifyOpenAPI(body); + return `"${encoded.replace(/"/g, "'")}"`; }, text: () => `"${String(body)}"`, - xmlOrCsv: () => `"${stringifyOpenAPI(body).replace(/"/g, '')}"`, + xml: () => { + // Convert JSON to XML if needed + return `"${convertBodyToXML(body)}"`; + }, + csv: () => `"${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(); - } + if (isXML(contentType)) return typeHandlers.xml(); + if (isCSV(contentType)) return typeHandlers.csv(); return typeHandlers.default(); }, }; + +/** + * Converts a body to XML format + */ +function convertBodyToXML(body: any): string { + // If body is already a string and looks like XML, return it as is + if (typeof body === 'string' && body.trim().startsWith('<')) { + return body; + } + + // If body is not an object, try to parse it as JSON + if (typeof body !== 'object' || body === null) { + try { + body = JSON.parse(body); + } catch { + // If parsing fails, return the original body + return body; + } + } + + return json2xml(body).replace(/"/g, '').replace(/\\n/g, '\n').replace(/\\t/g, '\t'); +}