Skip to content
Merged
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
32 changes: 28 additions & 4 deletions scripts/build-api-data.js
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ function flattenSpec(spec) {
if (!['get', 'post', 'put', 'delete', 'patch'].includes(method)) continue;
endpoints.push({
method: method.toUpperCase(),
path: pth,
path: pth.startsWith('/') ? pth : `/${pth}`,
summary: op.summary || '',
description: op.description || op.summary || '',
tags: op.tags || [],
Expand Down Expand Up @@ -128,7 +128,7 @@ function resolveRef(ref, spec) {
return node || null;
}

function extractRequestBody(requestBody, spec) {
function extractRequestBody(requestBody, spec, ctx = {}) {
if (!requestBody) return null;
const content = requestBody.content || {};
const contentType = Object.keys(content)[0] || 'application/json';
Expand All @@ -147,14 +147,38 @@ function extractRequestBody(requestBody, spec) {
}
const props = schema.properties || {};
const required = schema.required || [];
const properties = Object.entries(props).map(([name, propSchema]) => {
let properties = Object.entries(props).map(([name, propSchema]) => {
let resolved = propSchema.$ref ? resolveRef(propSchema.$ref, spec) || propSchema : propSchema;
const hasEnum = Array.isArray(resolved.enum) && resolved.enum.length > 0;
const type = resolved.type || 'string';
const format = resolved.format || null;
const displayType = hasEnum ? `enum<${type}>` : (format ? `${type}<${format}>` : type);
return { name, type: displayType, required: required.includes(name), description: resolved.description || '', ...(hasEnum && { enum: resolved.enum }) };
});

// Fallback for specs that declare `type: object` without `properties` —
// derive fields from the example block so the Try It modal can render
// 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.`);
}
}
}

return { contentType, description: requestBody.description || '', properties };
}

Expand Down Expand Up @@ -299,7 +323,7 @@ async function main() {
...((auth.length || ep.securityAuth?.length) && { auth: [...(ep.securityAuth || []), ...auth] }),
...(pathParams.length && { pathParams }),
...(queryParams.length && { queryParams }),
...(ep.requestBody && { requestBody: extractRequestBody(ep.requestBody, spec) }),
...(ep.requestBody && { requestBody: extractRequestBody(ep.requestBody, spec, { specName: name, method: ep.method, path: ep.path }) }),
responses: simplifyResponses(ep.responses, spec),
responseSchema: extractResponseSchema(ep.responses, spec),
};
Expand Down
13 changes: 7 additions & 6 deletions src/component/ApiReference/EndpointDetail.js
Original file line number Diff line number Diff line change
Expand Up @@ -558,11 +558,11 @@ function UrlBar({ endpoint, onTryIt }) {
// Fallback if no servers match (shouldn't happen, but safety net)
const effectiveServers = servers.length > 0 ? servers : allServers.slice(0, 1);

const [selectedBase, setSelectedBase] = useState(effectiveServers[0].url);
const [selectedBase, setSelectedBase] = useState((effectiveServers[0]?.url || '').replace(/\/+$/, ''));
const [copied, setCopied] = useState(false);

function copyUrl() {
const url = `${selectedBase}${endpoint.path}`;
const url = `${selectedBase.replace(/\/+$/, '')}${endpoint.path}`;
try {
navigator.clipboard.writeText(url).catch(() => {
const ta = document.createElement('textarea');
Expand All @@ -576,7 +576,7 @@ function UrlBar({ endpoint, onTryIt }) {

// Keep selectedBase in sync when endpoint changes
React.useEffect(() => {
setSelectedBase(effectiveServers[0].url);
setSelectedBase((effectiveServers[0]?.url || '').replace(/\/+$/, ''));
}, [endpoint.path, endpoint.method]);

const segments = parsePathSegments(endpoint.path || '');
Expand Down Expand Up @@ -610,9 +610,10 @@ function UrlBar({ endpoint, onTryIt }) {
flexShrink: 0,
}}
>
{effectiveServers.map((s) => (
<option key={s.url} value={s.url}>{s.url}</option>
))}
{effectiveServers.map((s) => {
const cleanUrl = s.url.replace(/\/+$/, '');
return <option key={cleanUrl} value={cleanUrl}>{cleanUrl}</option>;
})}
</select>

{/* Path segments */}
Expand Down
15 changes: 8 additions & 7 deletions src/component/ApiReference/TryItModal.js
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ function CodeHighlight({ code, language }) {
}

function buildCurl(endpoint, username, password, params, baseUrl) {
let url = `${baseUrl || endpoint.baseUrl}${endpoint.path}`;
let url = `${(baseUrl || endpoint.baseUrl || '').replace(/\/+$/, '')}${endpoint.path}`;
if (endpoint.pathParams) {
endpoint.pathParams.forEach((p) => {
url = url.replace(`{${p.name}}`, params[p.name] || `{${p.name}}`);
Expand Down Expand Up @@ -292,7 +292,7 @@ export default function TryItModal({ endpoint, onClose, selectedLang: selectedLa
const [respCopied, setRespCopied] = useState(false);
const [liveRespCopied, setLiveRespCopied] = useState(false);
const [localLang, setLocalLang] = useState('cURL');
const [selectedServer, setSelectedServer] = useState(effectiveServers[0]?.url || endpoint.baseUrl);
const [selectedServer, setSelectedServer] = useState((effectiveServers[0]?.url || endpoint.baseUrl || '').replace(/\/+$/, ''));
const selectedLang = selectedLangProp !== undefined ? selectedLangProp : localLang;
const setSelectedLang = onLangChange || setLocalLang;
const [langDropdownOpen, setLangDropdownOpen] = useState(false);
Expand Down Expand Up @@ -320,7 +320,7 @@ export default function TryItModal({ endpoint, onClose, selectedLang: selectedLa

// Update selected server when endpoint changes
useEffect(() => {
setSelectedServer(effectiveServers[0]?.url || endpoint.baseUrl || '');
setSelectedServer((effectiveServers[0]?.url || endpoint.baseUrl || '').replace(/\/+$/, ''));
}, [effectiveServers, endpoint.baseUrl]);


Expand All @@ -332,7 +332,7 @@ export default function TryItModal({ endpoint, onClose, selectedLang: selectedLa
setLoading(true);
setResponse(null);

let url = `${selectedServer}${endpoint.path}`;
let url = `${selectedServer.replace(/\/+$/, '')}${endpoint.path}`;
if (endpoint.pathParams) {
endpoint.pathParams.forEach((p) => {
url = url.replace(`{${p.name}}`, params[p.name] || '');
Expand Down Expand Up @@ -440,9 +440,10 @@ export default function TryItModal({ endpoint, onClose, selectedLang: selectedLa
maxWidth: '100%',
}}
>
{effectiveServers.map((s) => (
<option key={s.url} value={s.url}>{s.url}</option>
))}
{effectiveServers.map((s) => {
const cleanUrl = s.url.replace(/\/+$/, '');
return <option key={cleanUrl} value={cleanUrl}>{cleanUrl}</option>;
})}
</select>
) : (
<span style={{ fontSize: '13px', fontFamily: 'monospace', color: 'var(--ifm-color-emphasis-800)' }}>{selectedServer}</span>
Expand Down
2 changes: 1 addition & 1 deletion src/component/ApiReference/langUtils.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ export function generateCodeExample(endpoint, language, { username, password, pa
const methodLower = endpoint.method.toLowerCase();

// Build URL with path params substituted
let url = `${endpoint.baseUrl}${endpoint.path}`;
let url = `${(endpoint.baseUrl || '').replace(/\/+$/, '')}${endpoint.path}`;
if (endpoint.pathParams) {
endpoint.pathParams.forEach((p) => {
url = url.replace(`{${p.name}}`, (params && params[p.name]) || `{${p.name}}`);
Expand Down
Loading