diff --git a/scripts/build-api-data.js b/scripts/build-api-data.js index b362d7de1..ff55d5dab 100644 --- a/scripts/build-api-data.js +++ b/scripts/build-api-data.js @@ -117,6 +117,75 @@ function categorizeParams(parameters) { return { auth, pathParams, queryParams }; } +// Local patches for known-wrong examples in upstream specs. +// Each function takes the raw example and returns the corrected one. +// Remove the matching entry once the upstream YAML is fixed. +const REQUEST_BODY_EXAMPLE_OVERRIDES = { + // TE-17800: upstream uses `parent_folder_id`, real API expects `parent_id`. + // Rename both the key and its self-referential placeholder value. + 'Test Manager|POST|/api/v1/folder': (ex) => { + if (!ex || !Array.isArray(ex.folders)) return ex; + return { + ...ex, + folders: ex.folders.map((f) => { + if (!f || typeof f !== 'object' || !('parent_folder_id' in f)) return f; + const { parent_folder_id, ...rest } = f; + return { ...rest, parent_id: 'parent_id' }; + }), + }; + }, + // PUT /api/v1/folder is now in REQUEST_BODY_VARIANTS_OVERRIDES below — the + // upstream example omits the `action` discriminator and conflates rename/move. +}; + +// Variant overrides expose multiple body shapes for one endpoint. The modal +// renders a tab per variant; each variant has its own example + derived fields. +// Use this when an endpoint accepts distinct payload shapes selected by a +// discriminator (e.g. PUT /api/v1/folder uses `action: "update"` vs "move"). +const REQUEST_BODY_VARIANTS_OVERRIDES = { + // TE-17800: PUT /api/v1/folder serves two operations: + // - Rename: action="update", no parent_id/entity_type/serial_no + // - Move: action="move", adds parent_id + entity_type + serial_no + // The upstream YAML ships a single example that matches neither real payload. + 'Test Manager|PUT|/api/v1/folder': () => ([ + { + name: 'Rename', + example: { + id: 'folder_id', + action: 'update', + entity_id: 'project_id', + name: 'Test Folder - LambdaTest Demo', + }, + }, + { + name: 'Move', + example: { + id: 'folder_id', + action: 'move', + entity_id: 'project_id', + entity_type: 'project', + name: 'Test Folder - LambdaTest Demo', + parent_id: 'parent_id', + serial_no: 1, + }, + }, + ]), +}; + +// Infer flat field metadata from a primitive-only example object — same logic +// used by the legacy schema-less-fallback path, factored out for variant reuse. +function inferFieldsFromExample(example) { + if (!example || typeof example !== 'object' || Array.isArray(example)) return []; + return Object.entries(example).map(([name, value]) => { + let type = 'string'; + if (typeof value === 'number') type = Number.isInteger(value) ? 'integer' : 'number'; + else if (typeof value === 'boolean') type = 'boolean'; + else if (Array.isArray(value)) type = 'array'; + else if (typeof value === 'object' && value !== null) type = 'object'; + return { name, type, required: false, description: '' }; + }); +} + function resolveRef(ref, spec) { if (!ref || !ref.startsWith('#/')) return null; const parts = ref.slice(2).split('/'); @@ -145,6 +214,15 @@ function extractRequestBody(requestBody, spec, ctx = {}) { } schema = merged; } + // Resolve the example up front (with local override) so derived property names + // from the example fallback below also pick up the correction. + const rawExample = bodyContent.example ?? schema.example; + const overrideKey = (ctx.specName && ctx.method && ctx.path) + ? `${ctx.specName}|${ctx.method}|${ctx.path}` + : null; + const overrideFn = overrideKey ? REQUEST_BODY_EXAMPLE_OVERRIDES[overrideKey] : null; + const exampleValue = overrideFn ? overrideFn(rawExample) : rawExample; + const props = schema.properties || {}; const required = schema.required || []; let properties = Object.entries(props).map(([name, propSchema]) => { @@ -161,25 +239,38 @@ function extractRequestBody(requestBody, spec, ctx = {}) { // editable inputs. Inferred types and required flags are best-effort; // long-term fix is for spec authors to add proper `properties`. if (properties.length === 0) { - const example = bodyContent.example ?? schema.example; - if (example && typeof example === 'object' && !Array.isArray(example)) { - properties = Object.entries(example).map(([name, value]) => { - let type = 'string'; - if (typeof value === 'number') type = Number.isInteger(value) ? 'integer' : 'number'; - else if (typeof value === 'boolean') type = 'boolean'; - else if (Array.isArray(value)) type = 'array'; - else if (typeof value === 'object' && value !== null) type = 'object'; - return { name, type, required: false, description: '' }; - }); - if (properties.length > 0) { - const loc = ctx.method && ctx.path ? `${ctx.method} ${ctx.path}` : '(unknown endpoint)'; - const src = ctx.specName ? ` [${ctx.specName}]` : ''; - console.warn(`⚠️ ${loc}${src}: requestBody has no schema.properties; derived ${properties.length} field(s) from example. Spec should declare properties for accurate types/required/descriptions.`); - } + properties = inferFieldsFromExample(exampleValue); + if (properties.length > 0) { + const loc = ctx.method && ctx.path ? `${ctx.method} ${ctx.path}` : '(unknown endpoint)'; + const src = ctx.specName ? ` [${ctx.specName}]` : ''; + console.warn(`⚠️ ${loc}${src}: requestBody has no schema.properties; derived ${properties.length} field(s) from example. Spec should declare properties for accurate types/required/descriptions.`); } } - return { contentType, description: requestBody.description || '', properties }; + // Variant overrides: attach per-tab examples + derived fields when an + // endpoint serves multiple payload shapes (e.g. PUT folder rename vs move). + const variantsFn = overrideKey ? REQUEST_BODY_VARIANTS_OVERRIDES[overrideKey] : null; + const variants = variantsFn + ? variantsFn().map((v) => ({ + name: v.name, + example: v.example, + properties: inferFieldsFromExample(v.example), + })) + : null; + + // When variants are present, the first one becomes the canonical top-level + // example + properties so non-variant-aware consumers (e.g. the static + // CodeExamples panel) show a sensible default instead of stale spec data. + const finalExample = variants && variants.length > 0 ? variants[0].example : exampleValue; + const finalProperties = variants && variants.length > 0 ? variants[0].properties : properties; + + return { + contentType, + description: requestBody.description || '', + properties: finalProperties, + ...(finalExample !== undefined && { example: finalExample }), + ...(variants && variants.length > 0 && { variants }), + }; } function schemaToExample(schema, spec, seen = new Set()) { diff --git a/src/component/ApiReference/CodeExamples.js b/src/component/ApiReference/CodeExamples.js index d2a82dd19..7edef4944 100644 --- a/src/component/ApiReference/CodeExamples.js +++ b/src/component/ApiReference/CodeExamples.js @@ -74,6 +74,57 @@ function CodeHighlight({ code, language }) { ); } +// One stacked code panel — title bar with copy button (and optional language +// selector on the first panel of a multi-variant code section), code body below. +function CodePanel({ + title, code, language, + showLangSelector, selectedLang, setSelectedLang, + langDropdownOpen, setLangDropdownOpen, closeLangDropdown, langBtnRef, + onCopy, +}) { + const [copied, setCopied] = useState(false); + function handleCopy() { + onCopy(setCopied); + setCopied(true); + setTimeout(() => setCopied(false), 1500); + } + return ( +
+
+ {title} + {showLangSelector && ( + <> + setLangDropdownOpen((o) => !o)} + btnRef={langBtnRef} + /> + + + )} + +
+
+ +
+
+ ); +} + function formatResponse(value) { if (value == null) return ''; if (typeof value === 'string') { @@ -99,8 +150,28 @@ export default function CodeExamples({ endpoint, selectedLang: selectedLangProp, if (!endpoint) return null; + // When the endpoint ships requestBody.variants (e.g. PUT folder rename/move), + // render one code panel per variant stacked vertically. Each panel gets the + // variant's example + properties as a shimmed endpoint so the snippet matches + // that payload shape. Plain endpoints render a single panel as before. + const variants = endpoint.requestBody?.variants; + const summary = endpoint.name || endpoint.summary || ''; + const codePanels = (variants && variants.length > 0) + ? variants.map((v) => ({ + title: `${summary} (${v.name})`, + code: generateCodeExample( + { + ...endpoint, + requestBody: { ...endpoint.requestBody, properties: v.properties, example: v.example }, + }, + selectedLang, + ), + })) + : [{ title: endpoint.name, code: generateCodeExample(endpoint, selectedLang) }]; const langDef = LANGUAGES.find((l) => l.label === selectedLang) || LANGUAGES[0]; - const code = generateCodeExample(endpoint, selectedLang); + // Keep `code` available for the existing single-pane copy handler (used by + // the first panel's copy button when there are no variants). + const code = codePanels[0].code; const responses = endpoint.responses || {}; const responseTabs = Object.keys(responses).filter( @@ -129,36 +200,25 @@ export default function CodeExamples({ endpoint, selectedLang: selectedLangProp, return (
- {/* Code Examples Panel */} -
-
- {endpoint.name} - setLangDropdownOpen((o) => !o)} - btnRef={langBtnRef} - /> - - -
-
- -
-
+ {/* Code Examples Panel(s). When the endpoint has variants we render one + panel per variant, stacked vertically. The language selector lives in + the first panel's header and applies to all panels. */} + {codePanels.map((panel, idx) => ( + copyText(panel.code, setCopied)} + /> + ))} {/* Response Panel */} {responseTabs.length > 0 && ( diff --git a/src/component/ApiReference/EndpointDetail.js b/src/component/ApiReference/EndpointDetail.js index 1ccce20af..d493dd77d 100644 --- a/src/component/ApiReference/EndpointDetail.js +++ b/src/component/ApiReference/EndpointDetail.js @@ -764,23 +764,38 @@ export default function EndpointDetail({ endpoint, apiName, onTryIt, mobileCodeS )} - {/* Request Body */} - {endpoint.requestBody && Array.isArray(endpoint.requestBody.properties) && endpoint.requestBody.properties.length > 0 && ( -
-
-

Body

- {endpoint.requestBody.contentType && ( - {endpoint.requestBody.contentType} + {/* Request Body — one section per variant when present, otherwise the + plain body. Variants stack vertically so users see all payload shapes + (e.g. PUT folder rename/move) without a tab interaction. */} + {endpoint.requestBody && (() => { + const variants = endpoint.requestBody.variants; + const summary = endpoint.name || endpoint.summary || 'Body'; + const blocks = (variants && variants.length > 0) + ? variants.map((v) => ({ + title: `${summary} (${v.name})`, + props: v.properties, + })) + : (Array.isArray(endpoint.requestBody.properties) && endpoint.requestBody.properties.length > 0 + ? [{ title: 'Body', props: endpoint.requestBody.properties }] + : []); + if (blocks.length === 0) return null; + return blocks.map((block) => ( +
+
+

{block.title}

+ {endpoint.requestBody.contentType && ( + {endpoint.requestBody.contentType} + )} +
+ {endpoint.requestBody.description && ( +

)} -
- {endpoint.requestBody.description && ( -

- )} - {endpoint.requestBody.properties.map((prop, i) => ( - - ))} -
- )} + {block.props.map((prop, i) => ( + + ))} + + )); + })()} {/* Response section */} diff --git a/src/component/ApiReference/TryItModal.js b/src/component/ApiReference/TryItModal.js index 933a59ed0..60c8a394b 100644 --- a/src/component/ApiReference/TryItModal.js +++ b/src/component/ApiReference/TryItModal.js @@ -4,7 +4,7 @@ import { Highlight, themes } from 'prism-react-renderer'; import MethodBadge from './MethodBadge'; import InlineText from './InlineText'; import styles from './TryItModal.module.css'; -import { LANGUAGES, generateCodeExample, LangDropdownPortal, LangSelectorButton, coerceBodyValue } from './langUtils'; +import { LANGUAGES, generateCodeExample, LangDropdownPortal, LangSelectorButton, coerceBodyValue, detectFlattenedArrayBody } from './langUtils'; const githubWithGreenKeys = { ...themes.github, @@ -94,19 +94,58 @@ function buildCurl(endpoint, username, password, params, baseUrl) { const authLine = hasAuth ? ` \\\n --header 'Authorization: ${authHeader}'` : ''; const bodyProps = endpoint.requestBody?.properties || []; + const rawExample = endpoint.requestBody?.example; const contentType = endpoint.requestBody?.contentType || 'application/json'; + const isMultipart = contentType === 'multipart/form-data'; + const flattenedBody = !isMultipart ? detectFlattenedArrayBody(endpoint) : null; + const userFilledBody = flattenedBody + ? flattenedBody.innerFields.some((f) => params[`__body__${f.name}`]) + : bodyProps.some((p) => params[`__body__${p.name}`]); + const useRawExample = !isMultipart && !flattenedBody && !userFilledBody + && rawExample != null && typeof rawExample === 'object'; let bodyLine = ''; - if (bodyProps.length > 0) { - const bodyEntries = bodyProps.map((p) => [p.name, coerceBodyValue(params[`__body__${p.name}`] || '', p.type)]); - if (contentType === 'multipart/form-data') { + if (bodyProps.length > 0 || useRawExample || flattenedBody) { + if (isMultipart) { + const bodyEntries = bodyProps.map((p) => [p.name, coerceBodyValue(params[`__body__${p.name}`] || '', p.type)]); bodyLine = bodyEntries .filter(([, v]) => v) .map(([k, v]) => ` \\\n --form '${k}=${v}'`) .join(''); + } else if (flattenedBody) { + // Assemble inner object from per-field inputs (with example fallback), + // wrap under the spec's array key. + const inner = {}; + for (const f of flattenedBody.innerFields) { + const raw = params[`__body__${f.name}`]; + const fromEx = flattenedBody.innerExample[f.name]; + const val = (raw !== undefined && raw !== '') + ? coerceBodyValue(raw, f.type) + : fromEx; + if (val !== undefined && val !== '') inner[f.name] = val; + } + const bodyJson = { [flattenedBody.wrapperKey]: [inner] }; + bodyLine = ` \\\n --header 'Content-Type: application/json' \\\n --data '${JSON.stringify(bodyJson, null, 2)}'`; } else { - const bodyObj = Object.fromEntries(bodyEntries.filter(([, v]) => v)); - if (Object.keys(bodyObj).length) { - bodyLine = ` \\\n --header 'Content-Type: application/json' \\\n --data '${JSON.stringify(bodyObj)}'`; + let bodyJson; + if (useRawExample) { + bodyJson = rawExample; + } else { + // Empty inputs fall back to the spec example so editing one field + // doesn't blank out the others. + const fromExample = (p) => (rawExample && typeof rawExample === 'object') ? rawExample[p.name] : undefined; + const bodyEntries = bodyProps.map((p) => { + const raw = params[`__body__${p.name}`] || ''; + if (!raw) { + const ex = fromExample(p); + return [p.name, ex !== undefined ? ex : '']; + } + return [p.name, coerceBodyValue(raw, p.type)]; + }); + const bodyObj = Object.fromEntries(bodyEntries.filter(([, v]) => v !== '' && v !== undefined)); + if (Object.keys(bodyObj).length) bodyJson = bodyObj; + } + if (bodyJson !== undefined) { + bodyLine = ` \\\n --header 'Content-Type: application/json' \\\n --data '${JSON.stringify(bodyJson, null, 2)}'`; } } } @@ -284,7 +323,69 @@ export default function TryItModal({ endpoint, onClose, selectedLang: selectedLa const [username, setUsername] = useState(''); const [password, setPassword] = useState(''); - const [params, setParams] = useState({}); + // Variant-aware view of the request body: when the spec ships multiple + // payload shapes for one endpoint (e.g. PUT folder rename/move), the modal + // picks one tab and presents it as the effective body. Downstream helpers + // (buildCurl, handleSend, generateCodeExample) operate on `effectiveEndpoint` + // and stay variant-agnostic. + const variants = endpoint.requestBody?.variants || null; + const [selectedVariantIdx, setSelectedVariantIdx] = useState(0); + const selectedVariant = variants ? variants[selectedVariantIdx] : null; + const effectiveEndpoint = useMemo(() => { + if (!selectedVariant) return endpoint; + return { + ...endpoint, + requestBody: { + ...endpoint.requestBody, + properties: selectedVariant.properties, + example: selectedVariant.example, + }, + }; + }, [endpoint, selectedVariant]); + // Compute once at render time — both the params initializer and the body + // section need it, and `effectiveEndpoint` is stable for the variant's lifetime. + const flattenedBody = detectFlattenedArrayBody(effectiveEndpoint); + + // Build the per-variant prefill — also reused when the user switches tabs. + function computeInitialParams() { + const initial = {}; + if (flattenedBody) { + for (const f of flattenedBody.innerFields) { + const val = flattenedBody.innerExample[f.name]; + initial[`__body__${f.name}`] = val == null ? '' : String(val); + } + return initial; + } + const ex = effectiveEndpoint.requestBody?.example; + const props = effectiveEndpoint.requestBody?.properties || []; + if (ex && typeof ex === 'object' && !Array.isArray(ex)) { + for (const p of props) { + if (ex[p.name] === undefined) continue; + const v = ex[p.name]; + initial[`__body__${p.name}`] = v == null ? '' : String(v); + } + } + return initial; + } + // Prefill body fields with the spec example so the form is usable on first + // open. Flattened-array bodies expose inner primitive fields; plain bodies + // get one prefilled input per property. + const [params, setParams] = useState(computeInitialParams); + + // Reset & re-prefill body params whenever the active variant changes. + // Other params (path/query/auth) are not affected by variant switches. + useEffect(() => { + if (!variants) return; + setParams((prev) => { + const next = {}; + for (const k of Object.keys(prev)) { + if (!k.startsWith('__body__')) next[k] = prev[k]; + } + const fresh = computeInitialParams(); + return { ...next, ...fresh }; + }); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [selectedVariantIdx]); const [response, setResponse] = useState(null); const [loading, setLoading] = useState(false); const [activeResTab, setActiveResTab] = useState(null); @@ -347,18 +448,53 @@ export default function TryItModal({ endpoint, onClose, selectedLang: selectedLa ? `Basic ${safeBase64(`${username}:${password}`)}` : ''; - const bodyProps = endpoint.requestBody?.properties || []; - const contentType = endpoint.requestBody?.contentType || 'application/json'; + const bodyProps = effectiveEndpoint.requestBody?.properties || []; + const rawExample = effectiveEndpoint.requestBody?.example; + const contentType = effectiveEndpoint.requestBody?.contentType || 'application/json'; + const isMultipart = contentType === 'multipart/form-data'; + const flattenedBodyHS = !isMultipart ? detectFlattenedArrayBody(effectiveEndpoint) : null; + const userFilledBody = flattenedBodyHS + ? flattenedBodyHS.innerFields.some((f) => params[`__body__${f.name}`]) + : bodyProps.some((p) => params[`__body__${p.name}`]); + const useRawExample = !isMultipart && !flattenedBodyHS && !userFilledBody + && rawExample != null && typeof rawExample === 'object'; let fetchBody; let fetchHeaders = { ...(authHeader && { Authorization: authHeader }) }; - if (bodyProps.length > 0) { - if (contentType === 'multipart/form-data') { + if (bodyProps.length > 0 || useRawExample || flattenedBodyHS) { + if (isMultipart) { const fd = new FormData(); bodyProps.forEach((p) => { if (params[`__body__${p.name}`]) fd.append(p.name, params[`__body__${p.name}`]); }); fetchBody = fd; // Don't set Content-Type for FormData — browser sets it with boundary + } else if (flattenedBodyHS) { + const inner = {}; + for (const f of flattenedBodyHS.innerFields) { + const raw = params[`__body__${f.name}`]; + const fromEx = flattenedBodyHS.innerExample[f.name]; + const val = (raw !== undefined && raw !== '') + ? coerceBodyValue(raw, f.type) + : fromEx; + if (val !== undefined && val !== '') inner[f.name] = val; + } + fetchBody = JSON.stringify({ [flattenedBodyHS.wrapperKey]: [inner] }); + fetchHeaders['Content-Type'] = 'application/json'; + } else if (useRawExample) { + fetchBody = JSON.stringify(rawExample); + fetchHeaders['Content-Type'] = 'application/json'; } else { - const bodyObj = Object.fromEntries(bodyProps.map((p) => [p.name, coerceBodyValue(params[`__body__${p.name}`] || '', p.type)]).filter(([, v]) => v)); + // Empty inputs fall back to the spec example so partial edits don't + // strip the other example values. + const fromExample = (p) => (rawExample && typeof rawExample === 'object') ? rawExample[p.name] : undefined; + const bodyObj = Object.fromEntries( + bodyProps.map((p) => { + const raw = params[`__body__${p.name}`] || ''; + if (!raw) { + const ex = fromExample(p); + return [p.name, ex !== undefined ? ex : '']; + } + return [p.name, coerceBodyValue(raw, p.type)]; + }).filter(([, v]) => v !== '' && v !== undefined) + ); if (Object.keys(bodyObj).length) { fetchBody = JSON.stringify(bodyObj); fetchHeaders['Content-Type'] = 'application/json'; @@ -384,16 +520,16 @@ export default function TryItModal({ endpoint, onClose, selectedLang: selectedLa } } - const curlCode = buildCurl(endpoint, username, password, params, selectedServer); + const curlCode = buildCurl(effectiveEndpoint, username, password, params, selectedServer); const langDef = LANGUAGES.find((l) => l.label === selectedLang) || LANGUAGES[0]; const codeToShow = selectedLang === 'cURL' ? curlCode - : generateCodeExample(endpoint, selectedLang, { username, password, params }); + : generateCodeExample(effectiveEndpoint, selectedLang, { username, password, params }); const hasAuth = endpoint.auth && endpoint.auth.length > 0; const hasQuery = endpoint.queryParams && endpoint.queryParams.length > 0; const hasPath = endpoint.pathParams && endpoint.pathParams.length > 0; - const bodyProps = endpoint.requestBody?.properties || []; - const hasBody = bodyProps.length > 0; + const bodyProps = effectiveEndpoint.requestBody?.properties || []; + const hasBody = bodyProps.length > 0 || !!variants; // Static spec responses — always shown const specResponses = endpoint.responses || {}; @@ -548,18 +684,56 @@ export default function TryItModal({ endpoint, onClose, selectedLang: selectedLa {hasBody && ( - {bodyProps.map((p) => ( - updateParam(`__body__${p.name}`, v)} - placeholder={(p.type || '').toLowerCase().includes('array') ? 'e.g. ["val1", "val2"] or val1, val2' : undefined} - /> - ))} + {variants && ( +
+ {variants.map((v, idx) => { + const active = idx === selectedVariantIdx; + return ( + + ); + })} +
+ )} + {flattenedBody ? ( + flattenedBody.innerFields.map((f) => ( + updateParam(`__body__${f.name}`, v)} + /> + )) + ) : ( + bodyProps.map((p) => ( + updateParam(`__body__${p.name}`, v)} + placeholder={(p.type || '').toLowerCase().includes('array') ? 'e.g. ["val1", "val2"] or val1, val2' : undefined} + /> + )) + )}
)}
diff --git a/src/component/ApiReference/langUtils.js b/src/component/ApiReference/langUtils.js index 11255fdb9..fa5ef433f 100644 --- a/src/component/ApiReference/langUtils.js +++ b/src/component/ApiReference/langUtils.js @@ -38,6 +38,35 @@ export function coerceBodyValue(raw, type) { return raw; } +// Detect bodies shaped like `{ wrapper: [ { ...primitive fields } ] }` (e.g. +// Test Manager Create Folder). When matched, the form can render the inner +// object's primitive fields directly and the body assembly wraps them back +// into the array shape — giving the same per-field UX as a flat-body endpoint. +export function detectFlattenedArrayBody(endpoint) { + if (!endpoint || !endpoint.requestBody) return null; + const props = endpoint.requestBody.properties || []; + if (props.length !== 1) return null; + const only = props[0]; + if (only.type !== 'array') return null; + const example = endpoint.requestBody.example; + if (!example || typeof example !== 'object' || Array.isArray(example)) return null; + const arr = example[only.name]; + if (!Array.isArray(arr) || arr.length !== 1) return null; + const inner = arr[0]; + if (!inner || typeof inner !== 'object' || Array.isArray(inner)) return null; + // Bail when any inner value is an array or non-null object — the flat form + // can't represent nesting. `null` values are accepted (typed as string). + const innerFields = Object.entries(inner).map(([name, value]) => { + let type = 'string'; + if (typeof value === 'number') type = Number.isInteger(value) ? 'integer' : 'number'; + else if (typeof value === 'boolean') type = 'boolean'; + else if (Array.isArray(value) || (typeof value === 'object' && value !== null)) return null; + return { name, type, required: false, description: '' }; + }); + if (innerFields.some((f) => f === null)) return null; + return { wrapperKey: only.name, innerFields, innerExample: inner }; +} + // prism language mapping — only use languages bundled in prism-react-renderer export const LANGUAGES = [ { label: 'cURL', prism: 'clike' }, @@ -75,21 +104,52 @@ export function generateCodeExample(endpoint, language, { username, password, pa const bodyProps = endpoint.requestBody?.properties || []; const contentType = endpoint.requestBody?.contentType || 'application/json'; const isMultipart = contentType === 'multipart/form-data'; - const bodyExample = bodyProps.length > 0 - ? Object.fromEntries(bodyProps.map((p) => { - const raw = params && params[`__body__${p.name}`]; - let val; - if (raw) { - val = coerceBodyValue(raw, p.type); - } else { - val = p.type.includes('integer') || p.type.includes('number') ? 0 : - p.type.includes('boolean') ? true : - p.type.includes('array') ? [] : - `<${p.name}>`; - } - return [p.name, val]; - })) - : null; + const rawExample = endpoint.requestBody?.example; + const flattenedBody = !isMultipart ? detectFlattenedArrayBody(endpoint) : null; + const userFilledBody = flattenedBody + ? flattenedBody.innerFields.some((f) => params && params[`__body__${f.name}`]) + : bodyProps.some((p) => params && params[`__body__${p.name}`]); + // Use the spec's example block as the body when the user hasn't typed + // anything — keeps nested arrays/objects that property-based synthesis + // would flatten to `[]` (e.g. Test Manager Create Folder's `folders[]`). + const useRawExample = !isMultipart && !flattenedBody && !userFilledBody + && rawExample != null && typeof rawExample === 'object'; + let bodyExample; + if (flattenedBody) { + // Assemble the inner object from per-field inputs (with example fallback), + // wrap under the spec's array key so the snippet matches the spec shape. + const inner = {}; + for (const f of flattenedBody.innerFields) { + const raw = params && params[`__body__${f.name}`]; + const fromEx = flattenedBody.innerExample[f.name]; + const val = (raw !== undefined && raw !== '') + ? coerceBodyValue(raw, f.type) + : fromEx; + if (val !== undefined && val !== '') inner[f.name] = val; + } + bodyExample = { [flattenedBody.wrapperKey]: [inner] }; + } else if (useRawExample) { + bodyExample = rawExample; + } else if (bodyProps.length > 0) { + bodyExample = Object.fromEntries(bodyProps.map((p) => { + const raw = params && params[`__body__${p.name}`]; + const fromExample = rawExample && typeof rawExample === 'object' ? rawExample[p.name] : undefined; + let val; + if (raw) { + val = coerceBodyValue(raw, p.type); + } else if (fromExample !== undefined) { + val = fromExample; + } else { + val = p.type.includes('integer') || p.type.includes('number') ? 0 : + p.type.includes('boolean') ? true : + p.type.includes('array') ? [] : + `<${p.name}>`; + } + return [p.name, val]; + })); + } else { + bodyExample = null; + } switch (language) { case 'cURL': {