From 2da0ccf4ba6bb2bb0210d1e2d7b3f9b1194c44a1 Mon Sep 17 00:00:00 2001 From: Tal Gluck Date: Wed, 3 Dec 2025 12:03:29 +0100 Subject: [PATCH 1/3] update code sample generator for python --- packages/react-openapi/src/code-samples.ts | 149 ++++++++++++++++++++- 1 file changed, 144 insertions(+), 5 deletions(-) diff --git a/packages/react-openapi/src/code-samples.ts b/packages/react-openapi/src/code-samples.ts index 7c357b4fbc..e448fc29d0 100644 --- a/packages/react-openapi/src/code-samples.ts +++ b/packages/react-openapi/src/code-samples.ts @@ -156,7 +156,33 @@ ${headerString}${bodyString}`; syntax: 'python', generate: ({ method, url: { origin, path }, headers, body }) => { const contentType = headers?.['Content-Type']; - let code = `${isJSON(contentType) ? 'import json\n' : ''}import requests\n\n`; + const needsJsonImport = body && isJSON(contentType) && typeof body === 'string'; + + let code = ''; + + // Import statements + if (needsJsonImport) { + code += 'import json\n'; + } + code += 'import requests\n\n'; + + // Extract path parameters and create constants + const { extractedParams, processedPath } = extractPathParameters(path); + if (extractedParams.length > 0) { + extractedParams.forEach(param => { + code += `${param.constant} = "${param.placeholder}"\n`; + }); + code += '\n'; + } + + // Process headers to create better placeholders + const processedHeaders = processPythonHeaders(headers); + if (processedHeaders.constants.length > 0) { + processedHeaders.constants.forEach(constant => { + code += `${constant.name} = "${constant.placeholder}"\n`; + }); + code += '\n'; + } if (body) { const lines = BodyGenerators.getPythonBody(body, headers); @@ -169,17 +195,31 @@ ${headerString}${bodyString}`; } } + // Build the request + const urlStr = extractedParams.length > 0 + ? `f"${origin}${processedPath}"` + : `"${origin}${path}"`; + code += `response = requests.${method.toLowerCase()}(\n`; - code += indent(`"${origin}${path}",\n`, 4); + code += indent(`${urlStr},\n`, 4); - if (headers && Object.keys(headers).length > 0) { - code += indent(`headers=${stringifyOpenAPI(headers)},\n`, 4); + if (processedHeaders.headers && Object.keys(processedHeaders.headers).length > 0) { + code += indent(`headers={\n`, 4); + Object.entries(processedHeaders.headers).forEach(([key, value], index, array) => { + const isLast = index === array.length - 1; + code += indent(`"${key}": ${value}${isLast ? '' : ','}\n`, 8); + }); + code += indent(`},\n`, 4); } if (body) { if (body === 'files') { code += indent(`files=${body}\n`, 4); - } else if (isJSON(contentType)) { + } else if (isJSON(contentType) && isPlainObject(body)) { + // Use json parameter for dict objects + code += indent(`json=${body}\n`, 4); + } else if (isJSON(contentType) && needsJsonImport) { + // Use data=json.dumps() for JSON strings code += indent(`data=json.dumps(${body})\n`, 4); } else { code += indent(`data=${body}\n`, 4); @@ -372,7 +412,29 @@ const BodyGenerators = { } else if (isYAML(contentType)) { code += `yamlBody = \"\"\"\n${indent(yaml.dump(body), 4)}\"\"\"\n\n`; body = 'yamlBody'; + } else if (isJSON(contentType) && isPlainObject(body)) { + // For dict objects, return as-is to use with json= parameter + body = stringifyOpenAPI( + body, + (_key, value) => { + switch (value) { + case true: + return '$$__TRUE__$$'; + case false: + return '$$__FALSE__$$'; + case null: + return '$$__NULL__$$'; + default: + return value; + } + }, + 2 + ) + .replaceAll('"$$__TRUE__$$"', 'True') + .replaceAll('"$$__FALSE__$$"', 'False') + .replaceAll('"$$__NULL__$$"', 'None'); } else { + // For everything else (including JSON strings) body = stringifyOpenAPI( body, (_key, value) => { @@ -487,3 +549,80 @@ function buildHeredoc(lines: string[]): string { } return result; } + +/** + * Extracts path parameters and converts them to Python constants + */ +function extractPathParameters(path: string): { + extractedParams: Array<{ constant: string; placeholder: string; param: string }>; + processedPath: string; +} { + const extractedParams: Array<{ constant: string; placeholder: string; param: string }> = []; + let processedPath = path; + + // Find all path parameters in the format {paramName} + const paramMatches = path.match(/\{([^}]+)\}/g); + + if (paramMatches) { + paramMatches.forEach(match => { + const paramName = match.slice(1, -1); // Remove { and } + // Convert camelCase to SNAKE_CASE + const constantName = paramName + .replace(/([a-z])([A-Z])/g, '$1_$2') + .toUpperCase(); + const placeholder = ``; + + extractedParams.push({ + constant: constantName, + placeholder: placeholder, + param: paramName + }); + + // Replace {paramName} with {CONSTANT_NAME} for f-string + processedPath = processedPath.replace(match, `{${constantName}}`); + }); + } + + return { extractedParams, processedPath }; +} + +/** + * Processes headers to create Python constants and clean formatting + */ +function processPythonHeaders(headers?: Record): { + constants: Array<{ name: string; placeholder: string }>; + headers: Record; +} { + if (!headers) { + return { constants: [], headers: {} }; + } + + const constants: Array<{ name: string; placeholder: string }> = []; + const processedHeaders: Record = {}; + + Object.entries(headers).forEach(([key, value]) => { + if (key === 'Authorization' && value.includes('Bearer')) { + // Extract token constants + const constantName = 'API_TOKEN'; + const placeholder = ''; + constants.push({ name: constantName, placeholder }); + processedHeaders[key] = `f"Bearer {${constantName}}"`; + } else if (key === 'Authorization' && value.includes('Basic')) { + const constantName = 'API_TOKEN'; + const placeholder = ''; + constants.push({ name: constantName, placeholder }); + processedHeaders[key] = `f"Basic {${constantName}}"`; + } else if (value.includes('YOUR_') || value.includes('TOKEN')) { + // Generic token handling + const constantName = 'API_TOKEN'; + const placeholder = ''; + constants.push({ name: constantName, placeholder }); + processedHeaders[key] = `f"Bearer {${constantName}}"`; + } else { + // Regular headers + processedHeaders[key] = `"${value}"`; + } + }); + + return { constants, headers: processedHeaders }; +} From 2f05549c725ea2669819c90f651a2cdfa6786e54 Mon Sep 17 00:00:00 2001 From: Tal Gluck Date: Wed, 3 Dec 2025 12:19:41 +0100 Subject: [PATCH 2/3] update snippet generator --- packages/react-openapi/src/code-samples.ts | 153 ++++++++------------- 1 file changed, 57 insertions(+), 96 deletions(-) diff --git a/packages/react-openapi/src/code-samples.ts b/packages/react-openapi/src/code-samples.ts index e448fc29d0..8e16cf6823 100644 --- a/packages/react-openapi/src/code-samples.ts +++ b/packages/react-openapi/src/code-samples.ts @@ -37,30 +37,31 @@ export const codeSampleGenerators: CodeSampleGenerator[] = [ label: 'HTTP', syntax: 'http', generate: ({ method, url: { origin, path }, headers = {}, body }: CodeSampleInput) => { + // Process URL and headers to use consistent placeholder format + const processedPath = convertPathParametersToPlaceholders(path); + const processedHeaders = processHeadersWithPlaceholders(headers); + if (body) { // if we had a body add a content length header const bodyContent = body ? stringifyOpenAPI(body) : ''; // handle unicode chars with a text encoder const encoder = new TextEncoder(); - const bodyString = BodyGenerators.getHTTPBody(body, headers); + const bodyString = BodyGenerators.getHTTPBody(body, processedHeaders); if (bodyString) { body = bodyString; } - headers = { - ...headers, - 'Content-Length': encoder.encode(bodyContent).length.toString(), - }; + processedHeaders['Content-Length'] = encoder.encode(bodyContent).length.toString(); } - if (!headers.hasOwnProperty('Accept')) { - headers.Accept = '*/*'; + if (!processedHeaders.hasOwnProperty('Accept')) { + processedHeaders.Accept = '*/*'; } - const headerString = headers - ? `${Object.entries(headers) + const headerString = processedHeaders + ? `${Object.entries(processedHeaders) .map(([key, value]) => key.toLowerCase() !== 'host' ? `${key}: ${value}` : '' ) @@ -69,7 +70,7 @@ export const codeSampleGenerators: CodeSampleGenerator[] = [ const bodyString = body ? `\n${body}` : ''; - const httpRequest = `${method.toUpperCase()} ${decodeURI(path)} HTTP/1.1 + const httpRequest = `${method.toUpperCase()} ${decodeURI(processedPath)} HTTP/1.1 Host: ${origin.replaceAll(/https*:\/\//g, '')} ${headerString}${bodyString}`; @@ -87,15 +88,23 @@ ${headerString}${bodyString}`; lines.push(`--request ${method.toUpperCase()}`); } - lines.push(`--url '${origin}${path}'`); + // Process URL and headers to use consistent placeholder format + const processedUrl = convertPathParametersToPlaceholders(origin + path); + const processedHeaders = processHeadersWithPlaceholders(headers); + + lines.push(`--url '${processedUrl}'`); if (body) { - const bodyContent = BodyGenerators.getCurlBody(body, headers); + const bodyContent = BodyGenerators.getCurlBody(body, processedHeaders); if (bodyContent) { body = bodyContent.body; headers = bodyContent.headers; + } else { + headers = processedHeaders; } + } else { + headers = processedHeaders; } if (headers && Object.keys(headers).length > 0) { @@ -122,18 +131,26 @@ ${headerString}${bodyString}`; generate: ({ method, url: { origin, path }, headers, body }) => { let code = ''; + // Process URL and headers to use consistent placeholder format + const processedUrl = convertPathParametersToPlaceholders(origin + path); + const processedHeaders = processHeadersWithPlaceholders(headers); + if (body) { - const lines = BodyGenerators.getJavaScriptBody(body, headers); + const lines = BodyGenerators.getJavaScriptBody(body, processedHeaders); if (lines) { // add the generated code to the top code += lines.code; body = lines.body; headers = lines.headers; + } else { + headers = processedHeaders; } + } else { + headers = processedHeaders; } - code += `const response = await fetch('${origin}${path}', { + code += `const response = await fetch('${processedUrl}', { method: '${method.toUpperCase()}',\n`; if (headers && Object.keys(headers).length > 0) { @@ -166,23 +183,9 @@ ${headerString}${bodyString}`; } code += 'import requests\n\n'; - // Extract path parameters and create constants - const { extractedParams, processedPath } = extractPathParameters(path); - if (extractedParams.length > 0) { - extractedParams.forEach(param => { - code += `${param.constant} = "${param.placeholder}"\n`; - }); - code += '\n'; - } - - // Process headers to create better placeholders - const processedHeaders = processPythonHeaders(headers); - if (processedHeaders.constants.length > 0) { - processedHeaders.constants.forEach(constant => { - code += `${constant.name} = "${constant.placeholder}"\n`; - }); - code += '\n'; - } + // Process headers and URL to use consistent placeholder format + const processedUrl = convertPathParametersToPlaceholders(origin + path); + const processedHeaders = processHeadersWithPlaceholders(headers); if (body) { const lines = BodyGenerators.getPythonBody(body, headers); @@ -195,19 +198,14 @@ ${headerString}${bodyString}`; } } - // Build the request - const urlStr = extractedParams.length > 0 - ? `f"${origin}${processedPath}"` - : `"${origin}${path}"`; - code += `response = requests.${method.toLowerCase()}(\n`; - code += indent(`${urlStr},\n`, 4); + code += indent(`"${processedUrl}",\n`, 4); - if (processedHeaders.headers && Object.keys(processedHeaders.headers).length > 0) { + if (processedHeaders && Object.keys(processedHeaders).length > 0) { code += indent(`headers={\n`, 4); - Object.entries(processedHeaders.headers).forEach(([key, value], index, array) => { + Object.entries(processedHeaders).forEach(([key, value], index, array) => { const isLast = index === array.length - 1; - code += indent(`"${key}": ${value}${isLast ? '' : ','}\n`, 8); + code += indent(`"${key}": "${value}"${isLast ? '' : ','}\n`, 8); }); code += indent(`},\n`, 4); } @@ -551,78 +549,41 @@ function buildHeredoc(lines: string[]): string { } /** - * Extracts path parameters and converts them to Python constants + * Converts path parameters from {paramName} to YOUR_PARAM_NAME format */ -function extractPathParameters(path: string): { - extractedParams: Array<{ constant: string; placeholder: string; param: string }>; - processedPath: string; -} { - const extractedParams: Array<{ constant: string; placeholder: string; param: string }> = []; - let processedPath = path; - - // Find all path parameters in the format {paramName} - const paramMatches = path.match(/\{([^}]+)\}/g); - - if (paramMatches) { - paramMatches.forEach(match => { - const paramName = match.slice(1, -1); // Remove { and } - // Convert camelCase to SNAKE_CASE - const constantName = paramName - .replace(/([a-z])([A-Z])/g, '$1_$2') - .toUpperCase(); - const placeholder = ``; - - extractedParams.push({ - constant: constantName, - placeholder: placeholder, - param: paramName - }); - - // Replace {paramName} with {CONSTANT_NAME} for f-string - processedPath = processedPath.replace(match, `{${constantName}}`); - }); - } - - return { extractedParams, processedPath }; +function convertPathParametersToPlaceholders(urlPath: string): string { + return urlPath.replace(/\{([^}]+)\}/g, (match, paramName) => { + // Convert camelCase to UPPER_SNAKE_CASE + const placeholder = paramName + .replace(/([a-z])([A-Z])/g, '$1_$2') + .toUpperCase(); + return `YOUR_${placeholder}`; + }); } /** - * Processes headers to create Python constants and clean formatting + * Processes headers to use consistent placeholder format */ -function processPythonHeaders(headers?: Record): { - constants: Array<{ name: string; placeholder: string }>; - headers: Record; -} { +function processHeadersWithPlaceholders(headers?: Record): Record { if (!headers) { - return { constants: [], headers: {} }; + return {}; } - const constants: Array<{ name: string; placeholder: string }> = []; const processedHeaders: Record = {}; Object.entries(headers).forEach(([key, value]) => { if (key === 'Authorization' && value.includes('Bearer')) { - // Extract token constants - const constantName = 'API_TOKEN'; - const placeholder = ''; - constants.push({ name: constantName, placeholder }); - processedHeaders[key] = `f"Bearer {${constantName}}"`; + processedHeaders[key] = 'Bearer YOUR_API_TOKEN'; } else if (key === 'Authorization' && value.includes('Basic')) { - const constantName = 'API_TOKEN'; - const placeholder = ''; - constants.push({ name: constantName, placeholder }); - processedHeaders[key] = `f"Basic {${constantName}}"`; + processedHeaders[key] = 'Basic YOUR_API_TOKEN'; } else if (value.includes('YOUR_') || value.includes('TOKEN')) { - // Generic token handling - const constantName = 'API_TOKEN'; - const placeholder = ''; - constants.push({ name: constantName, placeholder }); - processedHeaders[key] = `f"Bearer {${constantName}}"`; + // Already in correct format or generic token + processedHeaders[key] = value.replace(/YOUR_SECRET_TOKEN|YOUR_TOKEN/g, 'YOUR_API_TOKEN'); } else { - // Regular headers - processedHeaders[key] = `"${value}"`; + // Regular headers - keep as-is + processedHeaders[key] = value; } }); - return { constants, headers: processedHeaders }; + return processedHeaders; } From 50cafe9ec6ea0e7503d76e3f2944b1516990129c Mon Sep 17 00:00:00 2001 From: Tal Gluck Date: Wed, 3 Dec 2025 14:24:35 +0100 Subject: [PATCH 3/3] linting --- packages/react-openapi/src/code-samples.ts | 27 +++++++++++----------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/packages/react-openapi/src/code-samples.ts b/packages/react-openapi/src/code-samples.ts index 8e16cf6823..6ebb14ffbc 100644 --- a/packages/react-openapi/src/code-samples.ts +++ b/packages/react-openapi/src/code-samples.ts @@ -71,7 +71,7 @@ export const codeSampleGenerators: CodeSampleGenerator[] = [ const bodyString = body ? `\n${body}` : ''; const httpRequest = `${method.toUpperCase()} ${decodeURI(processedPath)} HTTP/1.1 -Host: ${origin.replaceAll(/https*:\/\//g, '')} +Host: ${origin.replace(/https*:\/\//g, '')} ${headerString}${bodyString}`; return httpRequest; @@ -174,9 +174,9 @@ ${headerString}${bodyString}`; generate: ({ method, url: { origin, path }, headers, body }) => { const contentType = headers?.['Content-Type']; const needsJsonImport = body && isJSON(contentType) && typeof body === 'string'; - + let code = ''; - + // Import statements if (needsJsonImport) { code += 'import json\n'; @@ -428,9 +428,9 @@ const BodyGenerators = { }, 2 ) - .replaceAll('"$$__TRUE__$$"', 'True') - .replaceAll('"$$__FALSE__$$"', 'False') - .replaceAll('"$$__NULL__$$"', 'None'); + .replace(/"\\$\\$__TRUE__\\$\\$"/g, 'True') + .replace(/"\\$\\$__FALSE__\\$\\$"/g, 'False') + .replace(/"\\$\\$__NULL__\\$\\$"/g, 'None'); } else { // For everything else (including JSON strings) body = stringifyOpenAPI( @@ -449,9 +449,9 @@ const BodyGenerators = { }, 2 ) - .replaceAll('"$$__TRUE__$$"', 'True') - .replaceAll('"$$__FALSE__$$"', 'False') - .replaceAll('"$$__NULL__$$"', 'None'); + .replace(/"\\$\\$__TRUE__\\$\\$"/g, 'True') + .replace(/"\\$\\$__FALSE__\\$\\$"/g, 'False') + .replace(/"\\$\\$__NULL__\\$\\$"/g, 'None'); } return { body, code, headers }; @@ -554,9 +554,7 @@ function buildHeredoc(lines: string[]): string { function convertPathParametersToPlaceholders(urlPath: string): string { return urlPath.replace(/\{([^}]+)\}/g, (match, paramName) => { // Convert camelCase to UPPER_SNAKE_CASE - const placeholder = paramName - .replace(/([a-z])([A-Z])/g, '$1_$2') - .toUpperCase(); + const placeholder = paramName.replace(/([a-z])([A-Z])/g, '$1_$2').toUpperCase(); return `YOUR_${placeholder}`; }); } @@ -578,7 +576,10 @@ function processHeadersWithPlaceholders(headers?: Record): Recor processedHeaders[key] = 'Basic YOUR_API_TOKEN'; } else if (value.includes('YOUR_') || value.includes('TOKEN')) { // Already in correct format or generic token - processedHeaders[key] = value.replace(/YOUR_SECRET_TOKEN|YOUR_TOKEN/g, 'YOUR_API_TOKEN'); + processedHeaders[key] = value.replace( + /YOUR_SECRET_TOKEN|YOUR_TOKEN/g, + 'YOUR_API_TOKEN' + ); } else { // Regular headers - keep as-is processedHeaders[key] = value;