修复多层级返回递归问题
This commit is contained in:
parent
1fa9232c57
commit
a261632131
|
|
@ -1088,40 +1088,111 @@ const buildResponsePropertyDocLines = (schema, schemas) => {
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
|
|
||||||
const resolvedSchema = resolveSchema(schema, schemas)
|
const schemaContext = getSchemaTraversalContext(schema, schemas)
|
||||||
|
const resolvedSchema = schemaContext.resolvedSchema
|
||||||
|
|
||||||
if (resolvedSchema?.type === 'object' && resolvedSchema.properties) {
|
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') {
|
if (resolvedSchema?.type === 'array') {
|
||||||
const itemSchema = resolvedSchema.items
|
const itemLines = buildNestedResponsePropertyDocLines({
|
||||||
const resolvedItemSchema = resolveSchema(itemSchema, schemas)
|
schema: resolvedSchema.items,
|
||||||
|
schemas,
|
||||||
|
seenRefs: schemaContext.seenRefs,
|
||||||
|
parentPath: '',
|
||||||
|
omitRootArrayMarker: true,
|
||||||
|
})
|
||||||
|
|
||||||
if (resolvedItemSchema?.type === 'object' && resolvedItemSchema.properties) {
|
if (itemLines.length) {
|
||||||
return buildResponseObjectPropertyDocLines(itemSchema, schemas, 'Response item fields:')
|
return ['Response item fields:', ...itemLines]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
|
|
||||||
const buildResponseObjectPropertyDocLines = (schema, schemas, title) => {
|
const buildResponseObjectPropertyDocLines = ({ schema, schemas, seenRefs = new Set(), parentPath = '' }) => {
|
||||||
const resolvedSchema = resolveSchema(schema, schemas)
|
const schemaContext = getSchemaTraversalContext(schema, schemas, seenRefs)
|
||||||
|
const resolvedSchema = schemaContext.resolvedSchema
|
||||||
|
|
||||||
|
if (!resolvedSchema?.properties) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
const requiredKeys = new Set(resolvedSchema?.required || [])
|
const requiredKeys = new Set(resolvedSchema?.required || [])
|
||||||
const lines = [title]
|
const lines = []
|
||||||
|
|
||||||
for (const [propertyName, propertySchema] of Object.entries(resolvedSchema?.properties || {})) {
|
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 description = escapeComment(propertySchema.description || '')
|
||||||
const suffix = requiredKeys.has(propertyName) ? '' : '?'
|
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
|
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 buildParameterDocLine = (rootName, parameter, schemas) => {
|
||||||
const type = getJsDocType(parameter.schema, schemas)
|
const type = getJsDocType(parameter.schema, schemas)
|
||||||
const accessor = parameter.required
|
const accessor = parameter.required
|
||||||
|
|
@ -1185,28 +1256,60 @@ const getSchemaDisplayName = (schema, schemas) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const getExpandedResponseJsDocType = (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) {
|
if (!resolvedSchema) {
|
||||||
return 'any'
|
return 'any'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (schemaContext.isCircular) {
|
||||||
|
return getJsDocType(schema, schemas)
|
||||||
|
}
|
||||||
|
|
||||||
if (resolvedSchema.type === 'array') {
|
if (resolvedSchema.type === 'array') {
|
||||||
return appendNullable(
|
return appendNullable(
|
||||||
`Array<${getExpandedResponseObjectJsDocType(resolvedSchema.items, schemas)}>`,
|
`Array<${buildExpandedResponseJsDocType(resolvedSchema.items, schemas, schemaContext.seenRefs)}>`,
|
||||||
schema?.nullable || resolvedSchema.nullable,
|
nullable,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (resolvedSchema.type === 'object') {
|
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 getExpandedResponseObjectJsDocType = (
|
||||||
const resolvedSchema = resolveSchema(schema, schemas)
|
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) {
|
if (!resolvedSchema) {
|
||||||
return 'any'
|
return 'any'
|
||||||
|
|
@ -1222,14 +1325,15 @@ const getExpandedResponseObjectJsDocType = (schema, schemas) => {
|
||||||
const renderedName = renderJsDocObjectPropertyName(propertyName)
|
const renderedName = renderJsDocObjectPropertyName(propertyName)
|
||||||
const optionalToken = requiredKeys.has(propertyName) ? '' : '?'
|
const optionalToken = requiredKeys.has(propertyName) ? '' : '?'
|
||||||
|
|
||||||
return `${renderedName}${optionalToken}: ${getJsDocType(propertySchema, schemas)}`
|
return `${renderedName}${optionalToken}: ${buildExpandedResponseJsDocType(
|
||||||
|
propertySchema,
|
||||||
|
schemas,
|
||||||
|
schemaContext.seenRefs,
|
||||||
|
)}`
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
return appendNullable(
|
return appendNullable(`{ ${propertyEntries.join(', ')} }`, nullable)
|
||||||
`{ ${propertyEntries.join(', ')} }`,
|
|
||||||
schema?.nullable || resolvedSchema.nullable,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const getJsDocType = (schema, schemas) => {
|
const getJsDocType = (schema, schemas) => {
|
||||||
|
|
@ -1276,10 +1380,51 @@ const renderJsDocObjectPropertyName = (propertyName) => {
|
||||||
: JSON.stringify(propertyName)
|
: JSON.stringify(propertyName)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const formatResponseFieldType = (type) => {
|
||||||
|
return type.startsWith('{') ? type : `{${type}}`
|
||||||
|
}
|
||||||
|
|
||||||
const appendNullable = (type, nullable) => {
|
const appendNullable = (type, nullable) => {
|
||||||
return nullable ? `${type} | null` : type
|
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) => {
|
const resolveSchema = (schema, schemas) => {
|
||||||
if (!schema) {
|
if (!schema) {
|
||||||
return null
|
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 () => {
|
test('partial generation fails when retained generated files still collide with namespace exports', async () => {
|
||||||
const tempDir = await createTempDir()
|
const tempDir = await createTempDir()
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue