feat: 优化生成 API 的 JSDoc 结构与稳定性
- 递归展开请求体与响应体的对象类型注释 - 优化请求体/响应体字段说明的换行、分段与层级缩进 - 增加嵌套对象数组与循环引用 schema 的回归测试
This commit is contained in:
parent
a261632131
commit
c6482c7f08
|
|
@ -906,27 +906,52 @@ const generateOperationBlock = (
|
||||||
}
|
}
|
||||||
|
|
||||||
if (hasBody) {
|
if (hasBody) {
|
||||||
const bodyType = getJsDocType(bodySchemaInfo.schema, schemas)
|
const bodyType = getExpandedJsDocType(bodySchemaInfo.schema, schemas)
|
||||||
|
const bodySchemaName = getSchemaDisplayName(bodySchemaInfo.schema, schemas)
|
||||||
|
const bodyPropertyLines = buildRequestBodyPropertyDocLines({
|
||||||
|
rootName: signatureInfo.bodyArgName,
|
||||||
|
schema: bodySchemaInfo.schema,
|
||||||
|
schemas,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (hasParams) {
|
||||||
|
jsDocLines.push(' *')
|
||||||
|
}
|
||||||
|
|
||||||
jsDocLines.push(` * @param {${bodyType}} ${signatureInfo.bodyArgName} Request body`)
|
jsDocLines.push(` * @param {${bodyType}} ${signatureInfo.bodyArgName} Request body`)
|
||||||
|
|
||||||
for (const propertyLine of buildSchemaPropertyDocLines(
|
if (bodySchemaName) {
|
||||||
signatureInfo.bodyArgName,
|
jsDocLines.push(` * Request body schema: ${bodySchemaName}`)
|
||||||
bodySchemaInfo.schema,
|
}
|
||||||
schemas,
|
|
||||||
)) {
|
if (bodyPropertyLines.length) {
|
||||||
|
jsDocLines.push(' *')
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const propertyLine of bodyPropertyLines) {
|
||||||
jsDocLines.push(` * ${propertyLine}`)
|
jsDocLines.push(` * ${propertyLine}`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const responseSchemaInfo = getResponseSchemaInfo(operation.responses, schemas)
|
const responseSchemaInfo = getResponseSchemaInfo(operation.responses, schemas)
|
||||||
const returnType = `Promise<${getExpandedResponseJsDocType(responseSchemaInfo?.schema, schemas)}>`
|
const returnType = `Promise<${getExpandedJsDocType(responseSchemaInfo?.schema, schemas)}>`
|
||||||
const returnDescription = responseSchemaInfo?.displayName
|
const responsePropertyLines = buildResponsePropertyDocLines(responseSchemaInfo?.schema, schemas)
|
||||||
? ` Returns ${responseSchemaInfo.displayName}`
|
|
||||||
: ''
|
|
||||||
|
|
||||||
jsDocLines.push(` * @returns {${returnType}}${returnDescription}`)
|
if (hasParams || hasBody) {
|
||||||
|
jsDocLines.push(' *')
|
||||||
|
}
|
||||||
|
|
||||||
for (const propertyLine of buildResponsePropertyDocLines(responseSchemaInfo?.schema, schemas)) {
|
jsDocLines.push(` * @returns {${returnType}}`)
|
||||||
|
|
||||||
|
if (responseSchemaInfo?.displayName) {
|
||||||
|
jsDocLines.push(` * Response schema: ${responseSchemaInfo.displayName}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (responsePropertyLines.length) {
|
||||||
|
jsDocLines.push(' *')
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const propertyLine of responsePropertyLines) {
|
||||||
jsDocLines.push(` * ${propertyLine}`)
|
jsDocLines.push(` * ${propertyLine}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1084,6 +1109,31 @@ const getResponseSchemaInfo = (responses, schemas) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const buildResponsePropertyDocLines = (schema, schemas) => {
|
const buildResponsePropertyDocLines = (schema, schemas) => {
|
||||||
|
return buildStructuredPropertyDocLines({
|
||||||
|
title: 'Response fields:',
|
||||||
|
itemTitle: 'Response item fields:',
|
||||||
|
schema,
|
||||||
|
schemas,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const buildRequestBodyPropertyDocLines = ({ rootName, schema, schemas }) => {
|
||||||
|
return buildStructuredPropertyDocLines({
|
||||||
|
title: 'Request body fields:',
|
||||||
|
itemTitle: 'Request body item fields:',
|
||||||
|
schema,
|
||||||
|
schemas,
|
||||||
|
rootPath: rootName,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const buildStructuredPropertyDocLines = ({
|
||||||
|
title,
|
||||||
|
itemTitle,
|
||||||
|
schema,
|
||||||
|
schemas,
|
||||||
|
rootPath = '',
|
||||||
|
}) => {
|
||||||
if (!schema) {
|
if (!schema) {
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
|
|
@ -1093,33 +1143,42 @@ const buildResponsePropertyDocLines = (schema, schemas) => {
|
||||||
|
|
||||||
if (resolvedSchema?.type === 'object' && resolvedSchema.properties) {
|
if (resolvedSchema?.type === 'object' && resolvedSchema.properties) {
|
||||||
return [
|
return [
|
||||||
'Response fields:',
|
title,
|
||||||
...buildResponseObjectPropertyDocLines({
|
...buildStructuredObjectPropertyDocLines({
|
||||||
schema,
|
schema,
|
||||||
schemas,
|
schemas,
|
||||||
|
depth: 0,
|
||||||
|
parentPath: rootPath,
|
||||||
seenRefs: schemaContext.seenRefs,
|
seenRefs: schemaContext.seenRefs,
|
||||||
}),
|
}),
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
if (resolvedSchema?.type === 'array') {
|
if (resolvedSchema?.type === 'array') {
|
||||||
const itemLines = buildNestedResponsePropertyDocLines({
|
const itemLines = buildNestedStructuredPropertyDocLines({
|
||||||
schema: resolvedSchema.items,
|
schema: resolvedSchema.items,
|
||||||
schemas,
|
schemas,
|
||||||
|
depth: 0,
|
||||||
seenRefs: schemaContext.seenRefs,
|
seenRefs: schemaContext.seenRefs,
|
||||||
parentPath: '',
|
parentPath: rootPath,
|
||||||
omitRootArrayMarker: true,
|
omitRootArrayMarker: rootPath === '',
|
||||||
})
|
})
|
||||||
|
|
||||||
if (itemLines.length) {
|
if (itemLines.length) {
|
||||||
return ['Response item fields:', ...itemLines]
|
return [itemTitle, ...itemLines]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
|
|
||||||
const buildResponseObjectPropertyDocLines = ({ schema, schemas, seenRefs = new Set(), parentPath = '' }) => {
|
const buildStructuredObjectPropertyDocLines = ({
|
||||||
|
schema,
|
||||||
|
schemas,
|
||||||
|
depth = 0,
|
||||||
|
seenRefs = new Set(),
|
||||||
|
parentPath = '',
|
||||||
|
}) => {
|
||||||
const schemaContext = getSchemaTraversalContext(schema, schemas, seenRefs)
|
const schemaContext = getSchemaTraversalContext(schema, schemas, seenRefs)
|
||||||
const resolvedSchema = schemaContext.resolvedSchema
|
const resolvedSchema = schemaContext.resolvedSchema
|
||||||
|
|
||||||
|
|
@ -1133,17 +1192,18 @@ const buildResponseObjectPropertyDocLines = ({ schema, schemas, seenRefs = new S
|
||||||
for (const [propertyName, propertySchema] of Object.entries(resolvedSchema?.properties || {})) {
|
for (const [propertyName, propertySchema] of Object.entries(resolvedSchema?.properties || {})) {
|
||||||
const pathPrefix = parentPath ? `${parentPath}.` : ''
|
const pathPrefix = parentPath ? `${parentPath}.` : ''
|
||||||
const propertyPath = `${pathPrefix}${propertyName}`
|
const propertyPath = `${pathPrefix}${propertyName}`
|
||||||
const type = getExpandedResponseJsDocType(propertySchema, schemas)
|
const type = getJsDocType(propertySchema, schemas)
|
||||||
const description = escapeComment(propertySchema.description || '')
|
const description = escapeComment(propertySchema.description || '')
|
||||||
const suffix = requiredKeys.has(propertyName) ? '' : '?'
|
const suffix = requiredKeys.has(propertyName) ? '' : '?'
|
||||||
|
|
||||||
lines.push(
|
lines.push(
|
||||||
`- ${propertyPath}${suffix}: ${formatResponseFieldType(type)}${description ? ` ${description}` : ''}`,
|
`${buildResponseDocIndent(depth)}- ${propertyPath}${suffix}: ${formatResponseFieldType(type)}${description ? ` ${description}` : ''}`,
|
||||||
)
|
)
|
||||||
lines.push(
|
lines.push(
|
||||||
...buildNestedResponsePropertyDocLines({
|
...buildNestedStructuredPropertyDocLines({
|
||||||
schema: propertySchema,
|
schema: propertySchema,
|
||||||
schemas,
|
schemas,
|
||||||
|
depth: depth + 1,
|
||||||
seenRefs: schemaContext.seenRefs,
|
seenRefs: schemaContext.seenRefs,
|
||||||
parentPath: propertyPath,
|
parentPath: propertyPath,
|
||||||
}),
|
}),
|
||||||
|
|
@ -1153,9 +1213,10 @@ const buildResponseObjectPropertyDocLines = ({ schema, schemas, seenRefs = new S
|
||||||
return lines
|
return lines
|
||||||
}
|
}
|
||||||
|
|
||||||
const buildNestedResponsePropertyDocLines = ({
|
const buildNestedStructuredPropertyDocLines = ({
|
||||||
schema,
|
schema,
|
||||||
schemas,
|
schemas,
|
||||||
|
depth = 0,
|
||||||
seenRefs = new Set(),
|
seenRefs = new Set(),
|
||||||
parentPath,
|
parentPath,
|
||||||
omitRootArrayMarker = false,
|
omitRootArrayMarker = false,
|
||||||
|
|
@ -1168,18 +1229,20 @@ const buildNestedResponsePropertyDocLines = ({
|
||||||
}
|
}
|
||||||
|
|
||||||
if (resolvedSchema.type === 'object' && resolvedSchema.properties) {
|
if (resolvedSchema.type === 'object' && resolvedSchema.properties) {
|
||||||
return buildResponseObjectPropertyDocLines({
|
return buildStructuredObjectPropertyDocLines({
|
||||||
schema,
|
schema,
|
||||||
schemas,
|
schemas,
|
||||||
|
depth,
|
||||||
seenRefs: schemaContext.seenRefs,
|
seenRefs: schemaContext.seenRefs,
|
||||||
parentPath,
|
parentPath,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
if (resolvedSchema.type === 'array') {
|
if (resolvedSchema.type === 'array') {
|
||||||
return buildNestedResponsePropertyDocLines({
|
return buildNestedStructuredPropertyDocLines({
|
||||||
schema: resolvedSchema.items,
|
schema: resolvedSchema.items,
|
||||||
schemas,
|
schemas,
|
||||||
|
depth,
|
||||||
seenRefs: schemaContext.seenRefs,
|
seenRefs: schemaContext.seenRefs,
|
||||||
parentPath: parentPath
|
parentPath: parentPath
|
||||||
? `${parentPath}[]`
|
? `${parentPath}[]`
|
||||||
|
|
@ -1211,28 +1274,6 @@ const buildPositionalParameterDocLine = (parameter, schemas) => {
|
||||||
return `@param {${type}} ${accessor}${description ? ` ${description}` : ''}`
|
return `@param {${type}} ${accessor}${description ? ` ${description}` : ''}`
|
||||||
}
|
}
|
||||||
|
|
||||||
const buildSchemaPropertyDocLines = (rootName, schema, schemas) => {
|
|
||||||
const resolvedSchema = resolveSchema(schema, schemas)
|
|
||||||
const requiredKeys = new Set(resolvedSchema?.required || [])
|
|
||||||
const propertyLines = []
|
|
||||||
|
|
||||||
if (!resolvedSchema?.properties) {
|
|
||||||
return propertyLines
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const [propertyName, propertySchema] of Object.entries(resolvedSchema.properties)) {
|
|
||||||
const type = getJsDocType(propertySchema, schemas)
|
|
||||||
const accessor = requiredKeys.has(propertyName)
|
|
||||||
? `${rootName}.${propertyName}`
|
|
||||||
: `[${rootName}.${propertyName}]`
|
|
||||||
const description = escapeComment(propertySchema.description || '')
|
|
||||||
|
|
||||||
propertyLines.push(`@param {${type}} ${accessor}${description ? ` ${description}` : ''}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
return propertyLines
|
|
||||||
}
|
|
||||||
|
|
||||||
const getSchemaDisplayName = (schema, schemas) => {
|
const getSchemaDisplayName = (schema, schemas) => {
|
||||||
if (!schema) {
|
if (!schema) {
|
||||||
return ''
|
return ''
|
||||||
|
|
@ -1255,11 +1296,11 @@ const getSchemaDisplayName = (schema, schemas) => {
|
||||||
return ''
|
return ''
|
||||||
}
|
}
|
||||||
|
|
||||||
const getExpandedResponseJsDocType = (schema, schemas) => {
|
const getExpandedJsDocType = (schema, schemas) => {
|
||||||
return buildExpandedResponseJsDocType(schema, schemas)
|
return buildExpandedJsDocType(schema, schemas)
|
||||||
}
|
}
|
||||||
|
|
||||||
const buildExpandedResponseJsDocType = (
|
const buildExpandedJsDocType = (
|
||||||
schema,
|
schema,
|
||||||
schemas,
|
schemas,
|
||||||
seenRefs = new Set(),
|
seenRefs = new Set(),
|
||||||
|
|
@ -1279,13 +1320,13 @@ const buildExpandedResponseJsDocType = (
|
||||||
|
|
||||||
if (resolvedSchema.type === 'array') {
|
if (resolvedSchema.type === 'array') {
|
||||||
return appendNullable(
|
return appendNullable(
|
||||||
`Array<${buildExpandedResponseJsDocType(resolvedSchema.items, schemas, schemaContext.seenRefs)}>`,
|
`Array<${buildExpandedJsDocType(resolvedSchema.items, schemas, schemaContext.seenRefs)}>`,
|
||||||
nullable,
|
nullable,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (resolvedSchema.type === 'object') {
|
if (resolvedSchema.type === 'object') {
|
||||||
return getExpandedResponseObjectJsDocType(schema, schemas, schemaContext.seenRefs, nullable)
|
return getExpandedObjectJsDocType(schema, schemas, schemaContext.seenRefs, nullable)
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (resolvedSchema.type) {
|
switch (resolvedSchema.type) {
|
||||||
|
|
@ -1301,7 +1342,7 @@ const buildExpandedResponseJsDocType = (
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const getExpandedResponseObjectJsDocType = (
|
const getExpandedObjectJsDocType = (
|
||||||
schema,
|
schema,
|
||||||
schemas,
|
schemas,
|
||||||
seenRefs = new Set(),
|
seenRefs = new Set(),
|
||||||
|
|
@ -1325,7 +1366,7 @@ const getExpandedResponseObjectJsDocType = (
|
||||||
const renderedName = renderJsDocObjectPropertyName(propertyName)
|
const renderedName = renderJsDocObjectPropertyName(propertyName)
|
||||||
const optionalToken = requiredKeys.has(propertyName) ? '' : '?'
|
const optionalToken = requiredKeys.has(propertyName) ? '' : '?'
|
||||||
|
|
||||||
return `${renderedName}${optionalToken}: ${buildExpandedResponseJsDocType(
|
return `${renderedName}${optionalToken}: ${buildExpandedJsDocType(
|
||||||
propertySchema,
|
propertySchema,
|
||||||
schemas,
|
schemas,
|
||||||
schemaContext.seenRefs,
|
schemaContext.seenRefs,
|
||||||
|
|
@ -1384,6 +1425,10 @@ const formatResponseFieldType = (type) => {
|
||||||
return type.startsWith('{') ? type : `{${type}}`
|
return type.startsWith('{') ? type : `{${type}}`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const buildResponseDocIndent = (depth = 0) => {
|
||||||
|
return ' '.repeat(Math.max(0, depth))
|
||||||
|
}
|
||||||
|
|
||||||
const appendNullable = (type, nullable) => {
|
const appendNullable = (type, nullable) => {
|
||||||
return nullable ? `${type} | null` : type
|
return nullable ? `${type} | null` : type
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -505,15 +505,256 @@ test('response jsdoc expands nested array object fields', async () => {
|
||||||
|
|
||||||
assert.match(
|
assert.match(
|
||||||
alphaContent,
|
alphaContent,
|
||||||
/@returns\s+\{Promise<\{\s*total\?: number,\s*items\?: Array<\{\s*noteId\?: number,\s*author\?: \{\s*nickName\?: string\s*\}\s*\}>\s*\}>\}\s+Returns NoteSelectRankingResultPageResponse/s,
|
/@returns\s+\{Promise<\{\s*total\?: number,\s*items\?: Array<\{\s*noteId\?: number,\s*author\?: \{\s*nickName\?: string\s*\}\s*\}>\s*\}>\}/s,
|
||||||
)
|
)
|
||||||
|
assert.match(alphaContent, /Response schema: NoteSelectRankingResultPageResponse/)
|
||||||
|
assert.match(alphaContent, /\*\s*\n \* Response fields:/)
|
||||||
|
assert.match(alphaContent, /- items\?: \{Array<Object>\}/)
|
||||||
|
assert.match(alphaContent, / - items\[\]\.noteId\?: \{number\}/)
|
||||||
|
assert.match(alphaContent, / - items\[\]\.author\?: \{Object\}/)
|
||||||
|
assert.match(alphaContent, / - items\[\]\.author\.nickName\?: \{string\}/)
|
||||||
|
} finally {
|
||||||
|
await fs.rm(tempDir, { recursive: true, force: true })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
test('request body jsdoc expands nested array object fields', async () => {
|
||||||
|
const tempDir = await createTempDir()
|
||||||
|
|
||||||
|
try {
|
||||||
|
const swaggerPath = path.join(tempDir, 'swagger.json')
|
||||||
|
const outputDir = path.join(tempDir, 'generated')
|
||||||
|
|
||||||
|
await writeJson(swaggerPath, {
|
||||||
|
openapi: '3.0.0',
|
||||||
|
paths: {
|
||||||
|
'/api/v1/Alpha/Create': {
|
||||||
|
post: {
|
||||||
|
tags: ['Alpha'],
|
||||||
|
requestBody: {
|
||||||
|
content: {
|
||||||
|
'application/json': {
|
||||||
|
schema: {
|
||||||
|
$ref: '#/components/schemas/CreatePayload',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
responses: {
|
||||||
|
200: { description: 'OK' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
components: {
|
||||||
|
schemas: {
|
||||||
|
CreatePayload: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
title: {
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
items: {
|
||||||
|
type: 'array',
|
||||||
|
items: {
|
||||||
|
$ref: '#/components/schemas/CreateItem',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
CreateItem: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
id: {
|
||||||
|
type: 'integer',
|
||||||
|
},
|
||||||
|
author: {
|
||||||
|
$ref: '#/components/schemas/Author',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Author: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
nickName: {
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
await generateApiFiles({
|
||||||
|
projectRoot: tempDir,
|
||||||
|
swaggerUrl: swaggerPath,
|
||||||
|
swaggerTimeoutMs: 1000,
|
||||||
|
outputDir,
|
||||||
|
requestImport: '../request',
|
||||||
|
paramStyle: 'object',
|
||||||
|
modules: [],
|
||||||
|
cleanOutput: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
const alphaContent = await readFile(path.join(outputDir, 'alpha.js'))
|
||||||
|
|
||||||
assert.match(
|
assert.match(
|
||||||
alphaContent,
|
alphaContent,
|
||||||
/- items\?: \{Array<\{\s*noteId\?: number,\s*author\?: \{\s*nickName\?: string\s*\}\s*\}>\}/s,
|
/@param\s+\{\{\s*title\?: string,\s*items\?: Array<\{\s*id\?: number,\s*author\?: \{\s*nickName\?: string\s*\}\s*\}>\s*\}\}\s+data Request body/s,
|
||||||
)
|
)
|
||||||
assert.match(alphaContent, /- items\[\]\.noteId\?: \{number\}/)
|
assert.match(alphaContent, /Request body schema: CreatePayload/)
|
||||||
assert.match(alphaContent, /- items\[\]\.author\?: \{\s*nickName\?: string\s*\}/s)
|
assert.match(alphaContent, /\*\s*\n \* Request body fields:/)
|
||||||
assert.match(alphaContent, /- items\[\]\.author\.nickName\?: \{string\}/)
|
assert.match(alphaContent, /- data\.title\?: \{string\}/)
|
||||||
|
assert.match(alphaContent, /- data\.items\?: \{Array<Object>\}/)
|
||||||
|
assert.match(alphaContent, / - data\.items\[\]\.id\?: \{number\}/)
|
||||||
|
assert.match(alphaContent, / - data\.items\[\]\.author\?: \{Object\}/)
|
||||||
|
assert.match(alphaContent, / - data\.items\[\]\.author\.nickName\?: \{string\}/)
|
||||||
|
} finally {
|
||||||
|
await fs.rm(tempDir, { recursive: true, force: true })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
test('response jsdoc handles circular schemas without recursion errors', async () => {
|
||||||
|
const tempDir = await createTempDir()
|
||||||
|
|
||||||
|
try {
|
||||||
|
const swaggerPath = path.join(tempDir, 'swagger.json')
|
||||||
|
const outputDir = path.join(tempDir, 'generated')
|
||||||
|
|
||||||
|
await writeJson(swaggerPath, {
|
||||||
|
openapi: '3.0.0',
|
||||||
|
paths: {
|
||||||
|
'/api/v1/Tree/GetNode': {
|
||||||
|
get: {
|
||||||
|
tags: ['Tree'],
|
||||||
|
responses: {
|
||||||
|
200: {
|
||||||
|
description: 'OK',
|
||||||
|
content: {
|
||||||
|
'application/json': {
|
||||||
|
schema: {
|
||||||
|
$ref: '#/components/schemas/TreeNode',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
components: {
|
||||||
|
schemas: {
|
||||||
|
TreeNode: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
id: {
|
||||||
|
type: 'integer',
|
||||||
|
},
|
||||||
|
children: {
|
||||||
|
type: 'array',
|
||||||
|
items: {
|
||||||
|
$ref: '#/components/schemas/TreeNode',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
await assert.doesNotReject(
|
||||||
|
generateApiFiles({
|
||||||
|
projectRoot: tempDir,
|
||||||
|
swaggerUrl: swaggerPath,
|
||||||
|
swaggerTimeoutMs: 1000,
|
||||||
|
outputDir,
|
||||||
|
requestImport: '../request',
|
||||||
|
paramStyle: 'object',
|
||||||
|
modules: [],
|
||||||
|
cleanOutput: true,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
const treeContent = await readFile(path.join(outputDir, 'tree.js'))
|
||||||
|
|
||||||
|
assert.match(treeContent, /@returns\s+\{Promise<\{\s*id\?: number,\s*children\?: Array<Object>\s*\}>\}/s)
|
||||||
|
assert.match(treeContent, /- children\?: \{Array<Object>\}/)
|
||||||
|
assert.doesNotMatch(treeContent, /children\[\]\./)
|
||||||
|
} finally {
|
||||||
|
await fs.rm(tempDir, { recursive: true, force: true })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
test('request body jsdoc handles circular schemas without recursion errors', async () => {
|
||||||
|
const tempDir = await createTempDir()
|
||||||
|
|
||||||
|
try {
|
||||||
|
const swaggerPath = path.join(tempDir, 'swagger.json')
|
||||||
|
const outputDir = path.join(tempDir, 'generated')
|
||||||
|
|
||||||
|
await writeJson(swaggerPath, {
|
||||||
|
openapi: '3.0.0',
|
||||||
|
paths: {
|
||||||
|
'/api/v1/Tree/CreateNode': {
|
||||||
|
post: {
|
||||||
|
tags: ['Tree'],
|
||||||
|
requestBody: {
|
||||||
|
content: {
|
||||||
|
'application/json': {
|
||||||
|
schema: {
|
||||||
|
$ref: '#/components/schemas/TreeNode',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
responses: {
|
||||||
|
200: { description: 'OK' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
components: {
|
||||||
|
schemas: {
|
||||||
|
TreeNode: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
id: {
|
||||||
|
type: 'integer',
|
||||||
|
},
|
||||||
|
children: {
|
||||||
|
type: 'array',
|
||||||
|
items: {
|
||||||
|
$ref: '#/components/schemas/TreeNode',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
await assert.doesNotReject(
|
||||||
|
generateApiFiles({
|
||||||
|
projectRoot: tempDir,
|
||||||
|
swaggerUrl: swaggerPath,
|
||||||
|
swaggerTimeoutMs: 1000,
|
||||||
|
outputDir,
|
||||||
|
requestImport: '../request',
|
||||||
|
paramStyle: 'object',
|
||||||
|
modules: [],
|
||||||
|
cleanOutput: true,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
const treeContent = await readFile(path.join(outputDir, 'tree.js'))
|
||||||
|
|
||||||
|
assert.match(
|
||||||
|
treeContent,
|
||||||
|
/@param\s+\{\{\s*id\?: number,\s*children\?: Array<Object>\s*\}\}\s+data Request body/s,
|
||||||
|
)
|
||||||
|
assert.match(treeContent, /Request body schema: TreeNode/)
|
||||||
|
assert.match(treeContent, /- data\.children\?: \{Array<Object>\}/)
|
||||||
|
assert.doesNotMatch(treeContent, /data\.children\[\]\./)
|
||||||
} finally {
|
} finally {
|
||||||
await fs.rm(tempDir, { recursive: true, force: true })
|
await fs.rm(tempDir, { recursive: true, force: true })
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue