修复多层级返回递归问题
This commit is contained in:
parent
1fa9232c57
commit
a261632131
|
|
@ -1088,40 +1088,111 @@ const buildResponsePropertyDocLines = (schema, schemas) => {
|
|||
return []
|
||||
}
|
||||
|
||||
const resolvedSchema = resolveSchema(schema, schemas)
|
||||
const schemaContext = getSchemaTraversalContext(schema, schemas)
|
||||
const resolvedSchema = schemaContext.resolvedSchema
|
||||
|
||||
if (resolvedSchema?.type === 'object' && resolvedSchema.properties) {
|
||||
return buildResponseObjectPropertyDocLines(schema, schemas, 'Response fields:')
|
||||
return [
|
||||
'Response fields:',
|
||||
...buildResponseObjectPropertyDocLines({
|
||||
schema,
|
||||
schemas,
|
||||
seenRefs: schemaContext.seenRefs,
|
||||
}),
|
||||
]
|
||||
}
|
||||
|
||||
if (resolvedSchema?.type === 'array') {
|
||||
const itemSchema = resolvedSchema.items
|
||||
const resolvedItemSchema = resolveSchema(itemSchema, schemas)
|
||||
const itemLines = buildNestedResponsePropertyDocLines({
|
||||
schema: resolvedSchema.items,
|
||||
schemas,
|
||||
seenRefs: schemaContext.seenRefs,
|
||||
parentPath: '',
|
||||
omitRootArrayMarker: true,
|
||||
})
|
||||
|
||||
if (resolvedItemSchema?.type === 'object' && resolvedItemSchema.properties) {
|
||||
return buildResponseObjectPropertyDocLines(itemSchema, schemas, 'Response item fields:')
|
||||
if (itemLines.length) {
|
||||
return ['Response item fields:', ...itemLines]
|
||||
}
|
||||
}
|
||||
|
||||
return []
|
||||
}
|
||||
|
||||
const buildResponseObjectPropertyDocLines = (schema, schemas, title) => {
|
||||
const resolvedSchema = resolveSchema(schema, schemas)
|
||||
const buildResponseObjectPropertyDocLines = ({ schema, schemas, seenRefs = new Set(), parentPath = '' }) => {
|
||||
const schemaContext = getSchemaTraversalContext(schema, schemas, seenRefs)
|
||||
const resolvedSchema = schemaContext.resolvedSchema
|
||||
|
||||
if (!resolvedSchema?.properties) {
|
||||
return []
|
||||
}
|
||||
|
||||
const requiredKeys = new Set(resolvedSchema?.required || [])
|
||||
const lines = [title]
|
||||
const lines = []
|
||||
|
||||
for (const [propertyName, propertySchema] of Object.entries(resolvedSchema?.properties || {})) {
|
||||
const type = getJsDocType(propertySchema, schemas)
|
||||
const pathPrefix = parentPath ? `${parentPath}.` : ''
|
||||
const propertyPath = `${pathPrefix}${propertyName}`
|
||||
const type = getExpandedResponseJsDocType(propertySchema, schemas)
|
||||
const description = escapeComment(propertySchema.description || '')
|
||||
const suffix = requiredKeys.has(propertyName) ? '' : '?'
|
||||
|
||||
lines.push(`- ${propertyName}${suffix}: {${type}}${description ? ` ${description}` : ''}`)
|
||||
lines.push(
|
||||
`- ${propertyPath}${suffix}: ${formatResponseFieldType(type)}${description ? ` ${description}` : ''}`,
|
||||
)
|
||||
lines.push(
|
||||
...buildNestedResponsePropertyDocLines({
|
||||
schema: propertySchema,
|
||||
schemas,
|
||||
seenRefs: schemaContext.seenRefs,
|
||||
parentPath: propertyPath,
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
return lines
|
||||
}
|
||||
|
||||
const buildNestedResponsePropertyDocLines = ({
|
||||
schema,
|
||||
schemas,
|
||||
seenRefs = new Set(),
|
||||
parentPath,
|
||||
omitRootArrayMarker = false,
|
||||
}) => {
|
||||
const schemaContext = getSchemaTraversalContext(schema, schemas, seenRefs)
|
||||
const resolvedSchema = schemaContext.resolvedSchema
|
||||
|
||||
if (!resolvedSchema || schemaContext.isCircular) {
|
||||
return []
|
||||
}
|
||||
|
||||
if (resolvedSchema.type === 'object' && resolvedSchema.properties) {
|
||||
return buildResponseObjectPropertyDocLines({
|
||||
schema,
|
||||
schemas,
|
||||
seenRefs: schemaContext.seenRefs,
|
||||
parentPath,
|
||||
})
|
||||
}
|
||||
|
||||
if (resolvedSchema.type === 'array') {
|
||||
return buildNestedResponsePropertyDocLines({
|
||||
schema: resolvedSchema.items,
|
||||
schemas,
|
||||
seenRefs: schemaContext.seenRefs,
|
||||
parentPath: parentPath
|
||||
? `${parentPath}[]`
|
||||
: omitRootArrayMarker
|
||||
? ''
|
||||
: '[]',
|
||||
omitRootArrayMarker,
|
||||
})
|
||||
}
|
||||
|
||||
return []
|
||||
}
|
||||
|
||||
const buildParameterDocLine = (rootName, parameter, schemas) => {
|
||||
const type = getJsDocType(parameter.schema, schemas)
|
||||
const accessor = parameter.required
|
||||
|
|
@ -1185,28 +1256,60 @@ const getSchemaDisplayName = (schema, schemas) => {
|
|||
}
|
||||
|
||||
const getExpandedResponseJsDocType = (schema, schemas) => {
|
||||
const resolvedSchema = resolveSchema(schema, schemas)
|
||||
return buildExpandedResponseJsDocType(schema, schemas)
|
||||
}
|
||||
|
||||
const buildExpandedResponseJsDocType = (
|
||||
schema,
|
||||
schemas,
|
||||
seenRefs = new Set(),
|
||||
forceNullable = false,
|
||||
) => {
|
||||
const schemaContext = getSchemaTraversalContext(schema, schemas, seenRefs)
|
||||
const resolvedSchema = schemaContext.resolvedSchema
|
||||
const nullable = forceNullable || schema?.nullable || resolvedSchema?.nullable
|
||||
|
||||
if (!resolvedSchema) {
|
||||
return 'any'
|
||||
}
|
||||
|
||||
if (schemaContext.isCircular) {
|
||||
return getJsDocType(schema, schemas)
|
||||
}
|
||||
|
||||
if (resolvedSchema.type === 'array') {
|
||||
return appendNullable(
|
||||
`Array<${getExpandedResponseObjectJsDocType(resolvedSchema.items, schemas)}>`,
|
||||
schema?.nullable || resolvedSchema.nullable,
|
||||
`Array<${buildExpandedResponseJsDocType(resolvedSchema.items, schemas, schemaContext.seenRefs)}>`,
|
||||
nullable,
|
||||
)
|
||||
}
|
||||
|
||||
if (resolvedSchema.type === 'object') {
|
||||
return getExpandedResponseObjectJsDocType(schema, schemas)
|
||||
return getExpandedResponseObjectJsDocType(schema, schemas, schemaContext.seenRefs, nullable)
|
||||
}
|
||||
|
||||
return getJsDocType(schema, schemas)
|
||||
switch (resolvedSchema.type) {
|
||||
case 'integer':
|
||||
case 'number':
|
||||
return appendNullable('number', nullable)
|
||||
case 'boolean':
|
||||
return appendNullable('boolean', nullable)
|
||||
case 'string':
|
||||
return appendNullable('string', nullable)
|
||||
default:
|
||||
return appendNullable('any', nullable)
|
||||
}
|
||||
}
|
||||
|
||||
const getExpandedResponseObjectJsDocType = (schema, schemas) => {
|
||||
const resolvedSchema = resolveSchema(schema, schemas)
|
||||
const getExpandedResponseObjectJsDocType = (
|
||||
schema,
|
||||
schemas,
|
||||
seenRefs = new Set(),
|
||||
forceNullable = false,
|
||||
) => {
|
||||
const schemaContext = getSchemaTraversalContext(schema, schemas, seenRefs)
|
||||
const resolvedSchema = schemaContext.resolvedSchema
|
||||
const nullable = forceNullable || schema?.nullable || resolvedSchema?.nullable
|
||||
|
||||
if (!resolvedSchema) {
|
||||
return 'any'
|
||||
|
|
@ -1222,14 +1325,15 @@ const getExpandedResponseObjectJsDocType = (schema, schemas) => {
|
|||
const renderedName = renderJsDocObjectPropertyName(propertyName)
|
||||
const optionalToken = requiredKeys.has(propertyName) ? '' : '?'
|
||||
|
||||
return `${renderedName}${optionalToken}: ${getJsDocType(propertySchema, schemas)}`
|
||||
return `${renderedName}${optionalToken}: ${buildExpandedResponseJsDocType(
|
||||
propertySchema,
|
||||
schemas,
|
||||
schemaContext.seenRefs,
|
||||
)}`
|
||||
},
|
||||
)
|
||||
|
||||
return appendNullable(
|
||||
`{ ${propertyEntries.join(', ')} }`,
|
||||
schema?.nullable || resolvedSchema.nullable,
|
||||
)
|
||||
return appendNullable(`{ ${propertyEntries.join(', ')} }`, nullable)
|
||||
}
|
||||
|
||||
const getJsDocType = (schema, schemas) => {
|
||||
|
|
@ -1276,10 +1380,51 @@ const renderJsDocObjectPropertyName = (propertyName) => {
|
|||
: JSON.stringify(propertyName)
|
||||
}
|
||||
|
||||
const formatResponseFieldType = (type) => {
|
||||
return type.startsWith('{') ? type : `{${type}}`
|
||||
}
|
||||
|
||||
const appendNullable = (type, nullable) => {
|
||||
return nullable ? `${type} | null` : type
|
||||
}
|
||||
|
||||
const getSchemaTraversalContext = (schema, schemas, seenRefs = new Set()) => {
|
||||
if (!schema) {
|
||||
return {
|
||||
isCircular: false,
|
||||
resolvedSchema: null,
|
||||
seenRefs,
|
||||
}
|
||||
}
|
||||
|
||||
if (!schema.$ref) {
|
||||
return {
|
||||
isCircular: false,
|
||||
resolvedSchema: resolveSchema(schema, schemas),
|
||||
seenRefs,
|
||||
}
|
||||
}
|
||||
|
||||
const resolvedSchema = resolveSchema(schema, schemas)
|
||||
|
||||
if (seenRefs.has(schema.$ref)) {
|
||||
return {
|
||||
isCircular: true,
|
||||
resolvedSchema,
|
||||
seenRefs,
|
||||
}
|
||||
}
|
||||
|
||||
const nextSeenRefs = new Set(seenRefs)
|
||||
nextSeenRefs.add(schema.$ref)
|
||||
|
||||
return {
|
||||
isCircular: false,
|
||||
resolvedSchema,
|
||||
seenRefs: nextSeenRefs,
|
||||
}
|
||||
}
|
||||
|
||||
const resolveSchema = (schema, schemas) => {
|
||||
if (!schema) {
|
||||
return null
|
||||
|
|
|
|||
|
|
@ -423,6 +423,102 @@ test('generation automatically avoids collisions with module namespace export na
|
|||
}
|
||||
})
|
||||
|
||||
test('response 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/GetRanking': {
|
||||
get: {
|
||||
tags: ['Alpha'],
|
||||
responses: {
|
||||
200: {
|
||||
description: 'OK',
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: {
|
||||
$ref: '#/components/schemas/NoteSelectRankingResultPageResponse',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
components: {
|
||||
schemas: {
|
||||
NoteSelectRankingResultPageResponse: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
total: {
|
||||
type: 'integer',
|
||||
},
|
||||
items: {
|
||||
type: 'array',
|
||||
items: {
|
||||
$ref: '#/components/schemas/NoteSelectRankingResultItem',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
NoteSelectRankingResultItem: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
noteId: {
|
||||
type: 'integer',
|
||||
},
|
||||
author: {
|
||||
$ref: '#/components/schemas/NoteAuthor',
|
||||
},
|
||||
},
|
||||
},
|
||||
NoteAuthor: {
|
||||
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(
|
||||
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,
|
||||
)
|
||||
assert.match(
|
||||
alphaContent,
|
||||
/- items\?: \{Array<\{\s*noteId\?: number,\s*author\?: \{\s*nickName\?: string\s*\}\s*\}>\}/s,
|
||||
)
|
||||
assert.match(alphaContent, /- items\[\]\.noteId\?: \{number\}/)
|
||||
assert.match(alphaContent, /- items\[\]\.author\?: \{\s*nickName\?: string\s*\}/s)
|
||||
assert.match(alphaContent, /- items\[\]\.author\.nickName\?: \{string\}/)
|
||||
} finally {
|
||||
await fs.rm(tempDir, { recursive: true, force: true })
|
||||
}
|
||||
})
|
||||
|
||||
test('partial generation fails when retained generated files still collide with namespace exports', async () => {
|
||||
const tempDir = await createTempDir()
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue