Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
123 changes: 107 additions & 16 deletions scripts/build-api-data.js
Original file line number Diff line number Diff line change
Expand Up @@ -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('/');
Expand Down Expand Up @@ -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]) => {
Expand All @@ -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()) {
Expand Down
122 changes: 91 additions & 31 deletions src/component/ApiReference/CodeExamples.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<div style={{ border: '1px solid var(--ifm-color-emphasis-200)', borderRadius: '12px', overflow: 'hidden' }}>
<div className="flex items-center gap-2 px-4 py-2.5" style={{ background: 'var(--ifm-color-emphasis-100)', borderBottom: '1px solid var(--ifm-color-emphasis-200)' }}>
<span className="flex-1 text-sm font-medium text-gray-900 dark:text-gray-100 truncate">{title}</span>
{showLangSelector && (
<>
<LangSelectorButton
selectedLang={selectedLang}
open={langDropdownOpen}
onClick={() => setLangDropdownOpen((o) => !o)}
btnRef={langBtnRef}
/>
<LangDropdownPortal
open={langDropdownOpen}
anchorRef={langBtnRef}
langs={LANGUAGES}
selected={selectedLang}
onSelect={setSelectedLang}
onClose={closeLangDropdown}
/>
</>
)}
<button
onClick={handleCopy}
className="p-1.5 rounded-md border-0 bg-transparent appearance-none cursor-pointer text-gray-500 hover:text-gray-900 dark:text-gray-400 dark:hover:text-gray-100"
aria-label="Copy code"
>
{copied ? <CheckIcon /> : <CopyIcon />}
</button>
</div>
<div style={{ background: 'var(--ifm-background-color)' }}>
<CodeHighlight code={code} language={language} />
</div>
</div>
);
}

function formatResponse(value) {
if (value == null) return '';
if (typeof value === 'string') {
Expand All @@ -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(
Expand Down Expand Up @@ -129,36 +200,25 @@ export default function CodeExamples({ endpoint, selectedLang: selectedLangProp,

return (
<div className="space-y-4">
{/* Code Examples Panel */}
<div style={{ border: '1px solid var(--ifm-color-emphasis-200)', borderRadius: '12px', overflow: 'hidden' }}>
<div className="flex items-center gap-2 px-4 py-2.5" style={{ background: 'var(--ifm-color-emphasis-100)', borderBottom: '1px solid var(--ifm-color-emphasis-200)' }}>
<span className="flex-1 text-sm font-medium text-gray-900 dark:text-gray-100 truncate">{endpoint.name}</span>
<LangSelectorButton
selectedLang={selectedLang}
open={langDropdownOpen}
onClick={() => setLangDropdownOpen((o) => !o)}
btnRef={langBtnRef}
/>
<LangDropdownPortal
open={langDropdownOpen}
anchorRef={langBtnRef}
langs={LANGUAGES}
selected={selectedLang}
onSelect={setSelectedLang}
onClose={closeLangDropdown}
/>
<button
onClick={copyCode}
className="p-1.5 rounded-md border-0 bg-transparent appearance-none cursor-pointer text-gray-500 hover:text-gray-900 dark:text-gray-400 dark:hover:text-gray-100"
aria-label="Copy code"
>
{codeCopied ? <CheckIcon /> : <CopyIcon />}
</button>
</div>
<div style={{ background: 'var(--ifm-background-color)' }}>
<CodeHighlight code={code} language={langDef.prism} />
</div>
</div>
{/* 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) => (
<CodePanel
key={panel.title}
title={panel.title}
code={panel.code}
language={langDef.prism}
showLangSelector={idx === 0}
selectedLang={selectedLang}
setSelectedLang={setSelectedLang}
langDropdownOpen={langDropdownOpen}
setLangDropdownOpen={setLangDropdownOpen}
closeLangDropdown={closeLangDropdown}
langBtnRef={idx === 0 ? langBtnRef : null}
onCopy={(setCopied) => copyText(panel.code, setCopied)}
/>
))}

{/* Response Panel */}
{responseTabs.length > 0 && (
Expand Down
47 changes: 31 additions & 16 deletions src/component/ApiReference/EndpointDetail.js
Original file line number Diff line number Diff line change
Expand Up @@ -764,23 +764,38 @@ export default function EndpointDetail({ endpoint, apiName, onTryIt, mobileCodeS
</section>
)}

{/* Request Body */}
{endpoint.requestBody && Array.isArray(endpoint.requestBody.properties) && endpoint.requestBody.properties.length > 0 && (
<section className={styles.section}>
<div className="flex items-center justify-between mb-4 pb-2 border-b border-gray-200 dark:border-white/10">
<h2 className="text-xl font-semibold text-gray-900 dark:text-gray-100">Body</h2>
{endpoint.requestBody.contentType && (
<span className="text-xs text-gray-500 dark:text-gray-400 font-mono">{endpoint.requestBody.contentType}</span>
{/* 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) => (
<section key={block.title} className={styles.section}>
<div className="flex items-center justify-between mb-4 pb-2 border-b border-gray-200 dark:border-white/10">
<h2 className="text-xl font-semibold text-gray-900 dark:text-gray-100">{block.title}</h2>
{endpoint.requestBody.contentType && (
<span className="text-xs text-gray-500 dark:text-gray-400 font-mono">{endpoint.requestBody.contentType}</span>
)}
</div>
{endpoint.requestBody.description && (
<p className="text-sm text-gray-600 dark:text-gray-400 mb-4"><InlineText text={endpoint.requestBody.description} /></p>
)}
</div>
{endpoint.requestBody.description && (
<p className="text-sm text-gray-600 dark:text-gray-400 mb-4"><InlineText text={endpoint.requestBody.description} /></p>
)}
{endpoint.requestBody.properties.map((prop, i) => (
<ParamRow key={i} param={prop} />
))}
</section>
)}
{block.props.map((prop, i) => (
<ParamRow key={i} param={prop} />
))}
</section>
));
})()}

{/* Response section */}
<ResponseSection responses={endpoint.responses} responseSchema={endpoint.responseSchema} />
Expand Down
Loading