u
This commit is contained in:
@@ -518,23 +518,34 @@ function renderReadme(operations) {
|
||||
* @param {boolean} [markOptional] suffix non-required props with `?` (request bodies)
|
||||
* @returns {string}
|
||||
*/
|
||||
function summarizeSchema(schema, components = spec.components, depth = 0, markOptional = false) {
|
||||
function summarizeSchema(
|
||||
schema,
|
||||
components = spec.components,
|
||||
depth = 0,
|
||||
markOptional = false,
|
||||
aliases = null,
|
||||
skipRootAlias = false,
|
||||
) {
|
||||
if (!schema) return '';
|
||||
if (depth > 6) return 'object';
|
||||
if (depth > 6) return aliases?.compactTypes ? 'obj' : 'object';
|
||||
|
||||
const resolved = resolveSchema(schema, components);
|
||||
if (!resolved) return schemaRefName(schema) || '';
|
||||
if (resolved.$ref) return schemaRefName(resolved) || '';
|
||||
|
||||
if (resolved.type === 'array') {
|
||||
const item = summarizeSchema(resolved.items, components, depth + 1, markOptional) || 'any';
|
||||
const item = summarizeSchema(resolved.items, components, depth + 1, markOptional, aliases) || 'any';
|
||||
return `${item}[]`;
|
||||
}
|
||||
|
||||
if (resolved.type === 'object' || resolved.properties) {
|
||||
const shape = aliases && summarizeSchema(resolved, components, 0, markOptional);
|
||||
const alias = shape && aliases.get(`${markOptional ? 1 : 0}\0${shape}`);
|
||||
if (alias && !skipRootAlias) return alias;
|
||||
|
||||
const required = new Set(resolved.required || []);
|
||||
let entries = Object.entries(resolved.properties || {});
|
||||
if (entries.length === 0) return 'object';
|
||||
if (entries.length === 0) return aliases?.compactTypes ? 'obj' : 'object';
|
||||
|
||||
// Required props first, then optional ones (stable within each group).
|
||||
if (markOptional) {
|
||||
@@ -546,27 +557,91 @@ function summarizeSchema(schema, components = spec.components, depth = 0, markOp
|
||||
|
||||
const parts = entries.map(([name, prop]) => {
|
||||
const opt = markOptional && !required.has(name) ? '?' : '';
|
||||
const type = summarizeSchema(prop, components, depth + 1, markOptional) || 'any';
|
||||
const type = summarizeSchema(prop, components, depth + 1, markOptional, aliases) || 'any';
|
||||
return `${name}${opt}: ${type}`;
|
||||
});
|
||||
return `{ ${parts.join(', ')} }`;
|
||||
}
|
||||
|
||||
if (resolved.type) return resolved.type;
|
||||
if (resolved.type) {
|
||||
if (aliases?.compactTypes) {
|
||||
return {
|
||||
string: 'str',
|
||||
integer: 'int',
|
||||
number: 'num',
|
||||
boolean: 'bool',
|
||||
object: 'obj',
|
||||
}[resolved.type] || resolved.type;
|
||||
}
|
||||
return resolved.type;
|
||||
}
|
||||
return schemaRefName(schema) || '';
|
||||
}
|
||||
|
||||
function collectSchemaAliases(operations) {
|
||||
const shapes = new Map();
|
||||
const visit = (schema, markOptional) => {
|
||||
const resolved = resolveSchema(schema, spec.components);
|
||||
if (!resolved) return;
|
||||
|
||||
if (resolved.type === 'array') {
|
||||
visit(resolved.items, markOptional);
|
||||
return;
|
||||
}
|
||||
if (resolved.type !== 'object' && !resolved.properties) return;
|
||||
|
||||
const shape = summarizeSchema(resolved, spec.components, 0, markOptional);
|
||||
const key = `${markOptional ? 1 : 0}\0${shape}`;
|
||||
const current = shapes.get(key);
|
||||
if (current) {
|
||||
current.count += 1;
|
||||
} else {
|
||||
shapes.set(key, { count: 1, shape, schema: resolved, markOptional });
|
||||
}
|
||||
for (const prop of Object.values(resolved.properties || {})) {
|
||||
visit(prop, markOptional);
|
||||
}
|
||||
};
|
||||
|
||||
for (const { operation } of operations) {
|
||||
for (const param of operation.parameters || []) visit(param.schema, false);
|
||||
const content = operation.requestBody?.content || {};
|
||||
visit(content['application/json']?.schema, true);
|
||||
visit(content['multipart/form-data']?.schema, true);
|
||||
for (const [code, response] of Object.entries(operation.responses || {})) {
|
||||
if (code.startsWith('2')) {
|
||||
for (const media of Object.values(response.content || {})) {
|
||||
visit(media.schema, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const candidates = [...shapes.entries()]
|
||||
.filter(([, item]) => item.count >= 3 && item.shape.length >= 36)
|
||||
.sort((a, b) => b[1].shape.length - a[1].shape.length || a[0].localeCompare(b[0]));
|
||||
const aliases = new Map();
|
||||
const definitions = [];
|
||||
for (const [key, item] of candidates) {
|
||||
const alias = `T${definitions.length + 1}`;
|
||||
aliases.set(key, alias);
|
||||
definitions.push({ alias, ...item });
|
||||
}
|
||||
aliases.compactTypes = true;
|
||||
return { aliases, definitions };
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 paramType(param, aliases = null) {
|
||||
return summarizeSchema(param?.schema, spec.components, 0, false, aliases) || 'string';
|
||||
}
|
||||
|
||||
function summarizeOperationParams(operation) {
|
||||
function summarizeOperationParams(operation, aliases = null) {
|
||||
const parts = [];
|
||||
const params = operation.parameters || [];
|
||||
const pathParams = [];
|
||||
@@ -577,9 +652,9 @@ function summarizeOperationParams(operation) {
|
||||
const isOpt = !p.required;
|
||||
const optSuffix = isOpt ? '?' : '';
|
||||
if (p.in === 'path') {
|
||||
pathParams.push(`${p.name}: ${paramType(p)}`);
|
||||
pathParams.push(`${p.name}: ${paramType(p, aliases)}`);
|
||||
} else if (p.in === 'query') {
|
||||
queryParams.push(`${p.name}${optSuffix}: ${paramType(p)}`);
|
||||
queryParams.push(`${p.name}${optSuffix}: ${paramType(p, aliases)}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -596,11 +671,11 @@ function summarizeOperationParams(operation) {
|
||||
const content = operation.requestBody.content;
|
||||
if (content['application/json']?.schema) {
|
||||
const s = content['application/json'].schema;
|
||||
const sub = summarizeSchema(s, spec.components, 0, true);
|
||||
const sub = summarizeSchema(s, spec.components, 0, true, aliases);
|
||||
parts.push(`${bodyPrefix}: ${sub || 'json'}`);
|
||||
} else if (content['multipart/form-data']) {
|
||||
const formSchema = content['multipart/form-data'].schema;
|
||||
const formSub = summarizeSchema(formSchema, spec.components, 0, true);
|
||||
const formSub = summarizeSchema(formSchema, spec.components, 0, true, aliases);
|
||||
parts.push(`form: ${formSub || 'multipart'}`);
|
||||
} else {
|
||||
const ct = Object.keys(content)[0];
|
||||
@@ -611,7 +686,7 @@ function summarizeOperationParams(operation) {
|
||||
return parts.length ? parts.join(', ') : '—';
|
||||
}
|
||||
|
||||
function summarizeOperationReturns(operation) {
|
||||
function summarizeOperationReturns(operation, aliases = null) {
|
||||
const responses = operation.responses || {};
|
||||
const successCodes = Object.keys(responses)
|
||||
.filter((c) => c.startsWith('2'))
|
||||
@@ -643,7 +718,7 @@ function summarizeOperationReturns(operation) {
|
||||
continue;
|
||||
}
|
||||
const s = resp.content[ct]?.schema;
|
||||
const sub = summarizeSchema(s);
|
||||
const sub = summarizeSchema(s, spec.components, 0, false, aliases);
|
||||
if (sub) {
|
||||
parts.push(`${code} ${sub}`);
|
||||
} else {
|
||||
@@ -654,30 +729,37 @@ function summarizeOperationReturns(operation) {
|
||||
}
|
||||
|
||||
function renderRoutesOverview(operations, title = 'Dash API Routes Overview') {
|
||||
const { aliases, definitions } = collectSchemaAliases(operations);
|
||||
const lines = [];
|
||||
lines.push(`# ${title}`);
|
||||
lines.push('');
|
||||
lines.push(`Overview of all ${operations.length} API endpoints across all modules.`);
|
||||
lines.push('');
|
||||
lines.push('### Parameter Notation');
|
||||
lines.push('### Notation');
|
||||
lines.push('');
|
||||
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 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('Types: `str` string, `int` integer, `num` number, `bool` boolean, `obj` object; `T[]` array; `?` optional.');
|
||||
lines.push('Params: path values first, then `query:` and `body:` (`body?` = optional body). Responses: status code + JSON shape; `Tn` refers to a reusable type below.');
|
||||
if (definitions.length) {
|
||||
lines.push('');
|
||||
lines.push('### Reusable types');
|
||||
lines.push('');
|
||||
for (const def of definitions) {
|
||||
const shape = summarizeSchema(def.schema, spec.components, 0, def.markOptional, aliases, true);
|
||||
lines.push(`- \`${def.alias}\` = \`${shape}\``);
|
||||
}
|
||||
}
|
||||
lines.push('');
|
||||
lines.push('### Routes');
|
||||
lines.push('');
|
||||
for (const op of operations) {
|
||||
const params = summarizeOperationParams(op.operation, aliases);
|
||||
const returns = summarizeOperationReturns(op.operation, aliases);
|
||||
const method = op.method.toUpperCase();
|
||||
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(params === '—' ? '_none_' : `- Params: \`${params}\``);
|
||||
lines.push(returns === '—' ? '_no response body_' : `- Returns: \`${returns}\``);
|
||||
}
|
||||
lines.push('');
|
||||
return lines.join('\n');
|
||||
@@ -756,8 +838,7 @@ for (const apiPath of paths) {
|
||||
file: relFile.replace(/\\/g, '/'),
|
||||
summary: operation.summary || '',
|
||||
tags: operation.tags || [],
|
||||
params: summarizeOperationParams(operation),
|
||||
returns: summarizeOperationReturns(operation),
|
||||
operation,
|
||||
omittedFromOverview: isRouteOmitted(method, apiPath),
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user