Skip to content

Commit ae78fc5

Browse files
authored
Fix XML in code sample (#3030)
1 parent 40e8e69 commit ae78fc5

File tree

3 files changed

+119
-10
lines changed

3 files changed

+119
-10
lines changed

.changeset/early-singers-train.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@gitbook/react-openapi': patch
3+
---
4+
5+
Fix XML in code sample

packages/react-openapi/src/code-samples.test.ts

+69-1
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,23 @@ describe('curL code sample generator', () => {
110110
);
111111
});
112112

113+
it('should convert json to xml body properly', () => {
114+
const input: CodeSampleInput = {
115+
method: 'GET',
116+
url: 'https://example.com/path',
117+
headers: {
118+
'Content-Type': 'application/xml',
119+
},
120+
body: '{ "key": "value" }',
121+
};
122+
123+
const output = generator?.generate(input);
124+
125+
expect(output).toBe(
126+
"curl -L \\\n --url 'https://example.com/path' \\\n --header 'Content-Type: application/xml' \\\n --data-binary $'<?xml version=1.0?>\n <key>value</key>\n '"
127+
);
128+
});
129+
113130
it('should format application/graphql body properly', () => {
114131
const input: CodeSampleInput = {
115132
method: 'GET',
@@ -258,6 +275,23 @@ describe('javascript code sample generator', () => {
258275
);
259276
});
260277

278+
it('should convert json to xml body properly', () => {
279+
const input: CodeSampleInput = {
280+
method: 'GET',
281+
url: 'https://example.com/path',
282+
headers: {
283+
'Content-Type': 'application/xml',
284+
},
285+
body: '{ "key": "value" }',
286+
};
287+
288+
const output = generator?.generate(input);
289+
290+
expect(output).toBe(
291+
'const xml = `\n <?xml version=1.0?>\n <key>value</key>\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();'
292+
);
293+
});
294+
261295
it('should format application/graphql body properly', () => {
262296
const input: CodeSampleInput = {
263297
method: 'GET',
@@ -406,6 +440,23 @@ describe('python code sample generator', () => {
406440
);
407441
});
408442

443+
it('should convert json to xml body properly', () => {
444+
const input: CodeSampleInput = {
445+
method: 'GET',
446+
url: 'https://example.com/path',
447+
headers: {
448+
'Content-Type': 'application/xml',
449+
},
450+
body: '{ "key": "value" }',
451+
};
452+
453+
const output = generator?.generate(input);
454+
455+
expect(output).toBe(
456+
'import requests\n\nresponse = requests.get(\n "https://example.com/path",\n headers={"Content-Type":"application/xml"},\n data="<?xml version=1.0?>\\n<key>value</key>\\n"\n)\n\ndata = response.json()'
457+
);
458+
});
459+
409460
it('should format application/graphql body properly', () => {
410461
const input: CodeSampleInput = {
411462
method: 'GET',
@@ -514,7 +565,7 @@ describe('http code sample generator', () => {
514565
const output = generator?.generate(input);
515566

516567
expect(output).toBe(
517-
'GET /path HTTP/1.1\nHost: example.com\nContent-Type: application/x-www-form-urlencoded\nContent-Length: 15\nAccept: */*\n\n"key=value"'
568+
'GET /path HTTP/1.1\nHost: example.com\nContent-Type: application/x-www-form-urlencoded\nContent-Length: 15\nAccept: */*\n\n"key=\'value\'"'
518569
);
519570
});
520571

@@ -554,6 +605,23 @@ describe('http code sample generator', () => {
554605
);
555606
});
556607

608+
it('should convert json to xml body properly', () => {
609+
const input: CodeSampleInput = {
610+
method: 'GET',
611+
url: 'https://example.com/path',
612+
headers: {
613+
'Content-Type': 'application/xml',
614+
},
615+
body: '{ "key": "value" }',
616+
};
617+
618+
const output = generator?.generate(input);
619+
620+
expect(output).toBe(
621+
'GET /path HTTP/1.1\nHost: example.com\nContent-Type: application/xml\nContent-Length: 24\nAccept: */*\n\n"<?xml version=1.0?>\n<key>value</key>\n"'
622+
);
623+
});
624+
557625
it('should format application/graphql body properly', () => {
558626
const input: CodeSampleInput = {
559627
method: 'GET',

packages/react-openapi/src/code-samples.ts

+45-9
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
isText,
99
isXML,
1010
} from './contentTypeChecks';
11+
import { json2xml } from './json2xml';
1112
import { stringifyOpenAPI } from './stringifyOpenAPI';
1213

1314
export interface CodeSampleInput {
@@ -238,7 +239,10 @@ const BodyGenerators = {
238239
: String(body);
239240
} else if (isText(contentType)) {
240241
body = `--data '${String(body).replace(/"/g, '')}'`;
241-
} else if (isXML(contentType) || isCSV(contentType)) {
242+
} else if (isXML(contentType)) {
243+
// Convert to XML and ensure proper formatting
244+
body = `--data-binary $'${convertBodyToXML(body)}'`;
245+
} else if (isCSV(contentType)) {
242246
// We use --data-binary to avoid cURL converting newlines to \r\n
243247
body = `--data-binary $'${stringifyOpenAPI(body).replace(/"/g, '').replace(/\\n/g, '\n')}'`;
244248
} else if (isGraphQL(contentType)) {
@@ -312,7 +316,9 @@ const BodyGenerators = {
312316
body = 'formData';
313317
} else if (isXML(contentType)) {
314318
code += 'const xml = `\n';
315-
code += indent(String(body), 4);
319+
320+
// Convert JSON to XML if needed
321+
code += indent(convertBodyToXML(body), 4);
316322
code += '`;\n\n';
317323
body = 'xml';
318324
} else if (isText(contentType)) {
@@ -346,6 +352,11 @@ const BodyGenerators = {
346352
body = 'files';
347353
}
348354

355+
if (isXML(contentType)) {
356+
// Convert JSON to XML if needed
357+
body = convertBodyToXML(body);
358+
}
359+
349360
return { body, code, headers };
350361
},
351362
getHTTPBody: (body: any, headers?: Record<string, string>) => {
@@ -358,23 +369,48 @@ const BodyGenerators = {
358369
formUrlEncoded: () => {
359370
const encoded = isPlainObject(body)
360371
? Object.entries(body)
361-
.map(([key, value]) => `${key}=${String(value)}`)
372+
.map(([key, value]) => `${key}=${stringifyOpenAPI(value)}`)
362373
.join('&')
363-
: String(body);
364-
return `"${encoded}"`;
374+
: stringifyOpenAPI(body);
375+
return `"${encoded.replace(/"/g, "'")}"`;
365376
},
366377
text: () => `"${String(body)}"`,
367-
xmlOrCsv: () => `"${stringifyOpenAPI(body).replace(/"/g, '')}"`,
378+
xml: () => {
379+
// Convert JSON to XML if needed
380+
return `"${convertBodyToXML(body)}"`;
381+
},
382+
csv: () => `"${stringifyOpenAPI(body).replace(/"/g, '')}"`,
368383
default: () => `${stringifyOpenAPI(body, null, 2)}`,
369384
};
370385

371386
if (isPDF(contentType)) return typeHandlers.pdf();
372387
if (isFormUrlEncoded(contentType)) return typeHandlers.formUrlEncoded();
373388
if (isText(contentType)) return typeHandlers.text();
374-
if (isXML(contentType) || isCSV(contentType)) {
375-
return typeHandlers.xmlOrCsv();
376-
}
389+
if (isXML(contentType)) return typeHandlers.xml();
390+
if (isCSV(contentType)) return typeHandlers.csv();
377391

378392
return typeHandlers.default();
379393
},
380394
};
395+
396+
/**
397+
* Converts a body to XML format
398+
*/
399+
function convertBodyToXML(body: any): string {
400+
// If body is already a string and looks like XML, return it as is
401+
if (typeof body === 'string' && body.trim().startsWith('<')) {
402+
return body;
403+
}
404+
405+
// If body is not an object, try to parse it as JSON
406+
if (typeof body !== 'object' || body === null) {
407+
try {
408+
body = JSON.parse(body);
409+
} catch {
410+
// If parsing fails, return the original body
411+
return body;
412+
}
413+
}
414+
415+
return json2xml(body).replace(/"/g, '').replace(/\\n/g, '\n').replace(/\\t/g, '\t');
416+
}

0 commit comments

Comments
 (0)