diff --git a/.changeset/funny-falcons-brush.md b/.changeset/funny-falcons-brush.md new file mode 100644 index 000000000..d685423fe --- /dev/null +++ b/.changeset/funny-falcons-brush.md @@ -0,0 +1,5 @@ +--- +"@gitbook/react-openapi": patch +--- + +Add support for YAML content type in request body/example diff --git a/bun.lock b/bun.lock index c20a70354..a71cf8bb7 100644 --- a/bun.lock +++ b/bun.lock @@ -267,6 +267,7 @@ "@scalar/oas-utils": "^0.2.130", "clsx": "^2.1.1", "flatted": "^3.2.9", + "js-yaml": "^4.1.0", "json-xml-parse": "^1.3.0", "react-aria": "^3.37.0", "react-aria-components": "^1.6.0", @@ -274,6 +275,7 @@ "zustand": "^5.0.3", }, "devDependencies": { + "@types/js-yaml": "^4.0.9", "bun-types": "^1.1.20", "typescript": "^5.5.3", }, @@ -1377,6 +1379,8 @@ "@types/js-cookie": ["@types/js-cookie@3.0.6", "", {}, "sha512-wkw9yd1kEXOPnvEeEV1Go1MmxtBJL0RR79aOTAApecWFVu7w0NNXNqhcWgvw2YgZDYadliXkl14pa3WXw5jlCQ=="], + "@types/js-yaml": ["@types/js-yaml@4.0.9", "", {}, "sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg=="], + "@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="], "@types/jsontoxml": ["@types/jsontoxml@1.0.6", "", {}, "sha512-SoeEpHuqdZcpLxZr5uzpB6A/ICmzBOfluwMm59Bm6vDuiSRSv4M/z8hvS+YCRw4ZyGO5BhJ5oeJnrurSigg3Vw=="], diff --git a/packages/react-openapi/package.json b/packages/react-openapi/package.json index a740a9417..f52ad6c2d 100644 --- a/packages/react-openapi/package.json +++ b/packages/react-openapi/package.json @@ -20,9 +20,11 @@ "react-aria-components": "^1.6.0", "react-aria": "^3.37.0", "usehooks-ts": "^3.1.0", - "zustand": "^5.0.3" + "zustand": "^5.0.3", + "js-yaml": "^4.1.0" }, "devDependencies": { + "@types/js-yaml": "^4.0.9", "bun-types": "^1.1.20", "typescript": "^5.5.3" }, diff --git a/packages/react-openapi/src/InteractiveSection.tsx b/packages/react-openapi/src/InteractiveSection.tsx index db8a2662c..017957d1f 100644 --- a/packages/react-openapi/src/InteractiveSection.tsx +++ b/packages/react-openapi/src/InteractiveSection.tsx @@ -112,7 +112,7 @@ export function InteractiveSection(props: { event.stopPropagation(); }} > - {tabs.length > 1 ? ( + {tabs.length > 0 ? ( { it('should format application/json body properly', () => { const input: CodeSampleInput = { - method: 'GET', + method: 'POST', url: 'https://example.com/path', headers: { 'Content-Type': 'application/json', @@ -422,7 +422,7 @@ describe('python code sample generator', () => { 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=json.dumps({\n "key": "value",\n "truethy": True,\n "falsey": False,\n "nullish": None\n })\n)\n\ndata = response.json()' + 'import json\nimport requests\n\nresponse = requests.post(\n "https://example.com/path",\n headers={"Content-Type":"application/json"},\n data=json.dumps({\n "key": "value",\n "truethy": True,\n "falsey": False,\n "nullish": None\n })\n)\n\ndata = response.json()' ); }); diff --git a/packages/react-openapi/src/code-samples.ts b/packages/react-openapi/src/code-samples.ts index 8d855bbb6..e0c5e2055 100644 --- a/packages/react-openapi/src/code-samples.ts +++ b/packages/react-openapi/src/code-samples.ts @@ -1,3 +1,4 @@ +import yaml from 'js-yaml'; import { isCSV, isFormData, @@ -8,6 +9,7 @@ import { isPlainObject, isText, isXML, + isYAML, } from './contentTypeChecks'; import { json2xml } from './json2xml'; import { stringifyOpenAPI } from './stringifyOpenAPI'; @@ -154,7 +156,8 @@ ${headerString}${bodyString}`; label: 'Python', syntax: 'python', generate: ({ method, url, headers, body }) => { - let code = 'import requests\n\n'; + const contentType = headers?.['Content-Type']; + let code = `${isJSON(contentType) ? 'import json\n' : ''}import requests\n\n`; if (body) { const lines = BodyGenerators.getPythonBody(body, headers); @@ -174,7 +177,6 @@ ${headerString}${bodyString}`; code += indent(`headers=${stringifyOpenAPI(headers)},\n`, 4); } - const contentType = headers?.['Content-Type']; if (body) { if (body === 'files') { code += indent(`files=${body}\n`, 4); @@ -256,6 +258,8 @@ const BodyGenerators = { } else if (isPDF(contentType)) { // We use --data-binary to avoid cURL converting newlines to \r\n body = `--data-binary '@${String(body)}'`; + } else if (isYAML(contentType)) { + body = `--data-binary $'${yaml.dump(body).replace(/'/g, '').replace(/\\n/g, '\n')}'`; } else { body = `--data '${stringifyOpenAPI(body, null, 2).replace(/\\n/g, '\n')}'`; } @@ -325,6 +329,9 @@ const BodyGenerators = { code += indent(convertBodyToXML(body), 4); code += '`;\n\n'; body = 'xml'; + } else if (isYAML(contentType)) { + code += `const yamlBody = \`\n${indent(yaml.dump(body), 4)}\`;\n\n`; + body = 'yamlBody'; } else if (isText(contentType)) { body = stringifyOpenAPI(body, null, 2); } else { @@ -355,6 +362,9 @@ const BodyGenerators = { } else if (isXML(contentType)) { // Convert JSON to XML if needed body = JSON.stringify(convertBodyToXML(body)); + } else if (isYAML(contentType)) { + code += `yamlBody = \"\"\"\n${indent(yaml.dump(body), 4)}\"\"\"\n\n`; + body = 'yamlBody'; } else { body = stringifyOpenAPI( body, @@ -399,6 +409,7 @@ const BodyGenerators = { // Convert JSON to XML if needed return `"${convertBodyToXML(body)}"`; }, + yaml: () => `"${yaml.dump(body).replace(/"/g, '\\"')}"`, csv: () => `"${stringifyOpenAPI(body).replace(/"/g, '')}"`, default: () => `${stringifyOpenAPI(body, null, 2)}`, }; @@ -407,6 +418,7 @@ const BodyGenerators = { if (isFormUrlEncoded(contentType)) return typeHandlers.formUrlEncoded(); if (isText(contentType)) return typeHandlers.text(); if (isXML(contentType)) return typeHandlers.xml(); + if (isYAML(contentType)) return typeHandlers.yaml(); if (isCSV(contentType)) return typeHandlers.csv(); return typeHandlers.default(); diff --git a/packages/react-openapi/src/contentTypeChecks.ts b/packages/react-openapi/src/contentTypeChecks.ts index 06f106f42..e1b5debda 100644 --- a/packages/react-openapi/src/contentTypeChecks.ts +++ b/packages/react-openapi/src/contentTypeChecks.ts @@ -6,6 +6,10 @@ export function isXML(contentType?: string): boolean { return contentType?.toLowerCase().includes('application/xml') || false; } +export function isYAML(contentType?: string): boolean { + return contentType?.toLowerCase().includes('application/yaml') || false; +} + export function isGraphQL(contentType?: string): boolean { return contentType?.toLowerCase().includes('application/graphql') || false; }