This commit is contained in:
seb
2026-10-04 20:38:22 +02:00
parent e00533f9fa
commit ab9ce9b600
3 changed files with 1049 additions and 411 deletions

View File

@@ -505,85 +505,65 @@ function renderReadme(operations) {
return lines.join('\n');
}
function summarizeSchemaCompact(schema, components = spec.components, depth = 0) {
if (!schema || depth > 4) return '';
const resolved = resolveSchema(schema, components);
if (!resolved) return schemaRefName(schema) || '';
if (resolved.type === 'array') {
const item = summarizeSchemaCompact(resolved.items, components, depth + 1) || 'any';
return `${item}[]`;
}
if (resolved.type === 'object' || resolved.properties) {
const props = Object.keys(resolved.properties || {});
if (props.length === 0) return schemaRefName(schema) || 'object';
if (props.length === 1) {
const k = props[0];
const v = summarizeSchemaCompact(resolved.properties[k], components, depth + 1);
return v ? `{ ${k}: ${v} }` : `{ ${k} }`;
}
const inner = props.map((k) => {
const sub = resolved.properties[k];
if (sub && sub.type === 'array') {
const subItem = summarizeSchemaCompact(sub.items, components, depth + 1) || 'any';
return `${k}: ${subItem}[]`;
}
return k;
});
return `{ ${inner.join(', ')} }`;
}
if (resolved.type) return resolved.type;
return schemaRefName(schema) || '';
}
function summarizeParamSchema(schema, components) {
/**
* Compact one-line schema summary. Every leaf carries its explicit type
* (`string`, `integer`, `number`, `boolean`, `object`), arrays render as `T[]`,
* and objects render as `{ key: type, … }` at every nesting level.
*
* @param {object} [schema]
* @param {object} [components]
* @param {number} [depth]
* @param {boolean} [markOptional] suffix non-required props with `?` (request bodies)
* @returns {string}
*/
function summarizeSchema(schema, components = spec.components, depth = 0, markOptional = false) {
if (!schema) return '';
if (depth > 6) return 'object';
const resolved = resolveSchema(schema, components);
if (!resolved) return schemaRefName(schema) || '';
if (resolved.$ref) return schemaRefName(resolved) || '';
if (resolved.type === 'array') {
const item = summarizeParamSchema(resolved.items, components) || 'any';
const item = summarizeSchema(resolved.items, components, depth + 1, markOptional) || 'any';
return `${item}[]`;
}
if (resolved.type === 'object' || resolved.properties) {
const required = new Set(resolved.required || []);
const props = Object.keys(resolved.properties || {});
if (props.length === 0) {
return schemaRefName(schema) || 'object';
let entries = Object.entries(resolved.properties || {});
if (entries.length === 0) return 'object';
// Required props first, then optional ones (stable within each group).
if (markOptional) {
entries = entries
.map(([name], i) => ({ name, i }))
.sort((a, b) => (required.has(a.name) ? 0 : 1) - (required.has(b.name) ? 0 : 1) || a.i - b.i)
.map(({ name }) => [name, resolved.properties[name]]);
}
// Prioritize required props before optional props
const sortedProps = [...props].sort((a, b) => {
const reqA = required.has(a) ? 0 : 1;
const reqB = required.has(b) ? 0 : 1;
return reqA - reqB;
});
const formattedProps = sortedProps.map((k) => {
const isOpt = !required.has(k);
const propSchema = resolved.properties[k];
const optSuffix = isOpt ? '?' : '';
if (propSchema?.type === 'array') {
const subItem = summarizeParamSchema(propSchema.items, components) || 'any';
return `${k}${optSuffix}: ${subItem}[]`;
}
if (propSchema?.type === 'object' && propSchema.properties) {
const sub = summarizeParamSchema(propSchema, components);
return `${k}${optSuffix}: ${sub}`;
}
if ((k === 'sku' || sortedProps.length === 1) && propSchema?.type) {
return `${k}${optSuffix}: ${propSchema.type}`;
}
return `${k}${optSuffix}`;
const parts = entries.map(([name, prop]) => {
const opt = markOptional && !required.has(name) ? '?' : '';
const type = summarizeSchema(prop, components, depth + 1, markOptional) || 'any';
return `${name}${opt}: ${type}`;
});
return `{ ${formattedProps.join(', ')} }`;
return `{ ${parts.join(', ')} }`;
}
if (resolved.type) return resolved.type;
return schemaRefName(schema) || '';
}
/**
* Always-visible type for a path/query parameter, so declarations are uniform.
* Falls back to `string` only when the parameter schema omits a type entirely.
* @param {object} param
* @returns {string}
*/
function paramType(param) {
return summarizeSchema(param?.schema, spec.components) || 'string';
}
function summarizeOperationParams(operation) {
const parts = [];
const params = operation.parameters || [];
@@ -592,13 +572,12 @@ function summarizeOperationParams(operation) {
for (const p of params) {
if (p.in === 'header' || p.in === 'cookie') continue;
const type = p.schema?.type ? `:${p.schema.type}` : '';
const isOpt = !p.required;
const optSuffix = isOpt ? '?' : '';
if (p.in === 'path') {
pathParams.push(`${p.name}${type}`);
pathParams.push(`${p.name}: ${paramType(p)}`);
} else if (p.in === 'query') {
queryParams.push(`${p.name}${optSuffix}${type}`);
queryParams.push(`${p.name}${optSuffix}: ${paramType(p)}`);
}
}
@@ -615,11 +594,11 @@ function summarizeOperationParams(operation) {
const content = operation.requestBody.content;
if (content['application/json']?.schema) {
const s = content['application/json'].schema;
const sub = summarizeParamSchema(s, spec.components);
const sub = summarizeSchema(s, spec.components, 0, true);
parts.push(`${bodyPrefix}: ${sub || 'json'}`);
} else if (content['multipart/form-data']) {
const formSchema = content['multipart/form-data'].schema;
const formSub = summarizeParamSchema(formSchema, spec.components);
const formSub = summarizeSchema(formSchema, spec.components, 0, true);
parts.push(`form: ${formSub || 'multipart'}`);
} else {
const ct = Object.keys(content)[0];
@@ -662,7 +641,7 @@ function summarizeOperationReturns(operation) {
continue;
}
const s = resp.content[ct]?.schema;
const sub = summarizeSchemaCompact(s);
const sub = summarizeSchema(s);
if (sub) {
parts.push(`${code} ${sub}`);
} else {
@@ -680,20 +659,23 @@ function renderRoutesOverview(operations, title = 'Dash API Routes Overview') {
lines.push('');
lines.push('### Parameter Notation');
lines.push('');
lines.push('- **Required parameters** have no `?` (e.g. `sku:string`, `categoryId`, `query: si:string`, `body: { name }`).');
lines.push('- **Optional parameters** are marked with `?` (e.g. `query: categoryId?:integer`, `track_stock?`, `body?: ...`).');
lines.push('- **Every value carries its type** — `string`, `integer`, `number`, `boolean`, `object`, arrays as `type[]`.');
lines.push('- **Required parameters** have no `?` (e.g. `sku: string`, `id: integer`, `query: si: string`, `body: { name: string }`).');
lines.push('- **Optional parameters** are marked with `?` (e.g. `query: limit?: integer`, `body: { sort?: integer }`, `body?: { … }`).');
lines.push('- **Path parameters** match `{param}` in the route URL.');
lines.push('- **Query parameters** are prefixed with `query: ...`.');
lines.push('- **Request bodies** are prefixed with `body: ...` (or `body?: ...` if optional).');
lines.push('- **Request bodies** are prefixed with `body: ...` (or `body?: ...` if the whole body is optional).');
lines.push('- **Response objects** use the same notation, so `201 { article: { … } }` means a JSON object with an `article` property.');
lines.push('');
lines.push('### Routes');
lines.push('');
lines.push('| Method | Route | Params | Return Type | Summary |');
lines.push('|:---|:---|:---|:---|:---|');
for (const op of operations) {
const method = op.method.toUpperCase();
const summary = (op.summary || '').replace(/[\r\n|]+/g, ' ').trim();
const paramsStr = op.params === '—' ? '—' : `\`${op.params.replace(/\|/g, '\\|')}\``;
const returnsStr = op.returns === '—' ? '—' : `\`${op.returns.replace(/\|/g, '\\|')}\``;
lines.push(`| \`${method}\` | \`${op.path}\` | ${paramsStr} | ${returnsStr} | ${summary} |`);
const summary = (op.summary || '').replace(/[\r\n]+/g, ' ').trim();
const heading = summary ? `${summary} — \`${method} ${op.path}\`` : `\`${method} ${op.path}\``;
lines.push(`#### ${heading}`);
lines.push(op.params === '—' ? '_none_' : `- Params: \`${op.params}\``);
lines.push(op.returns === '—' ? '_no response body_' : `- Returns: \`${op.returns}\``);
}
lines.push('');
return lines.join('\n');