Skip to content

Commit a2c2ae1

Browse files
committed
feat: update ESLint configuration to disable 'no-template-curly-in-string' rule, upgrade openapi-specification-types version to 0.3.0, and enhance parser functionality with additional parameter handling
1 parent afb4078 commit a2c2ae1

19 files changed

Lines changed: 174 additions & 121 deletions

File tree

eslint.config.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ export default antfu(
99
},
1010
{
1111
rules: {
12+
'no-template-curly-in-string': 'off',
1213
'ts/explicit-function-return-type': 'off',
1314
'ts/no-namespace': 'off',
1415
},

packages/core/vitest.config.ts

Lines changed: 0 additions & 9 deletions
This file was deleted.

packages/parser/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
"scripts": {
2727
"build": "tsdown",
2828
"dev": "tsdown --watch",
29+
"test": "vitest run",
2930
"typecheck": "tsc --noEmit",
3031
"automd": "automd",
3132
"prepublishOnly": "nr build"

packages/parser/src/create-parser.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,9 +36,10 @@ export function createParser(pathHandler: PathHandler) {
3636

3737
provide({ interfaces, functions })
3838
transformBaseURL(source)
39-
transformDefinitions(source.definitions)
39+
if (source.definitions)
40+
transformDefinitions(source.definitions)
4041

41-
traversePaths(source.paths, (config) => {
42+
traversePaths(source.paths ?? {}, (config) => {
4243
pathHandler(config, inject() as ParserContext)
4344
})
4445

packages/parser/src/parses/common.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import type { OpenAPISpecificationV2 } from 'openapi-specification-types'
2-
import type { OpenAPISpecificationV3 } from 'openapi-specification-types/index-v3'
1+
import type { OpenAPISpecificationV2, OpenAPISpecificationV3 } from 'openapi-specification-types'
2+
33
import { swagger2ToSwagger3 } from '@genapi/transform'
44

55
/**

packages/parser/src/parses/method.ts

Lines changed: 33 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
/* eslint-disable ts/ban-ts-comment */
21
import type { StatementField, StatementInterface } from '@genapi/shared'
32
import type { Parameter } from 'openapi-specification-types'
43
import type { PathMethod } from '../traverse'
@@ -18,12 +17,14 @@ export type { InSchemas }
1817
*/
1918
export function parseMethodParameters({ method, parameters, path }: PathMethod, schemas?: InSchemas) {
2019
const { config: userConfig } = inject()
21-
const requestConfigs = {
22-
body: [] as StatementField[],
23-
formData: [] as StatementField[],
24-
path: [] as StatementField[],
25-
query: [] as StatementField[],
26-
header: [] as StatementField[],
20+
const requestConfigs: Record<string, StatementField[]> = {
21+
body: [],
22+
formData: [],
23+
path: [],
24+
query: [],
25+
header: [],
26+
cookie: [],
27+
querystring: [],
2728
}
2829

2930
const config = {
@@ -32,14 +33,17 @@ export function parseMethodParameters({ method, parameters, path }: PathMethod,
3233
interfaces: [] as StatementInterface[],
3334
}
3435

35-
for (const parameter of parameters)
36-
requestConfigs[parameter.in].push(parseParameterFiled(parameter))
36+
for (const parameter of parameters) {
37+
const key = parameter.in
38+
if (key in requestConfigs)
39+
requestConfigs[key].push(parseParameterFiled(parameter))
40+
}
3741

38-
for (const [inType, properties] of Object.entries(requestConfigs) as [Parameter['in'], StatementField[]][]) {
42+
for (const [inType, properties] of Object.entries(requestConfigs)) {
3943
if (properties.length === 0)
4044
continue
4145

42-
const name = toUndefField(inType, schemas)
46+
const name = toUndefField(inType as Parameter['in'], schemas)
4347

4448
if (inType !== 'path')
4549
config.options.push(name)
@@ -56,7 +60,7 @@ export function parseMethodParameters({ method, parameters, path }: PathMethod,
5660
continue
5761
}
5862

59-
if (['header', 'path', 'query'].includes(inType)) {
63+
if (['header', 'path', 'query', 'cookie', 'querystring'].includes(inType)) {
6064
const typeName = varName([method, path, inType])
6165
config.interfaces.push({ name: typeName, properties, export: true })
6266
const required = inType === 'path' || isRequiredParameter(properties) || userConfig?.parametersRequired
@@ -87,25 +91,30 @@ export function parseMethodParameters({ method, parameters, path }: PathMethod,
8791

8892
export function parseMethodMetadata({ method, path, responses, options: meta }: PathMethod) {
8993
const { configRead, interfaces } = inject()
94+
const metaAny = meta as { consumes?: string[] }
9095
const comments = [
9196
meta.summary && `@summary ${meta.summary}`,
9297
meta.description && `@description ${meta.description}`,
9398
`@method ${method}`,
94-
meta.tags && `@tags ${meta.tags.join(' | ') || '-'}`,
95-
meta.consumes && `@consumes ${meta.consumes.join('; ') || '-'}`,
96-
]
99+
meta.tags?.length ? `@tags ${meta.tags.join(' | ') || '-'}` : undefined,
100+
metaAny.consumes?.length ? `@consumes ${metaAny.consumes.join('; ') || '-'}` : undefined,
101+
].filter((c): c is string => typeof c === 'string')
97102

98103
const name = camelCase(`${method}/${path}`)
99104

100105
const url = `${path.replace(/(\{)/g, '${paths.')}`
101-
const responseSchema
102-
// @ts-expect-error
103-
= responses.default?.content?.['application/json']?.schema
104-
// @ts-expect-error
105-
|| responses['200']?.content?.['application/json']?.schema
106-
|| responses['200']?.schema
107-
|| responses['200']
108-
const responseType = responseSchema ? parseSchemaType(responseSchema) : 'void'
106+
function hasContent(r: unknown): r is { content?: Record<string, { schema?: unknown }> } {
107+
return r != null && typeof r === 'object' && 'content' in r
108+
}
109+
const resDefault = responses.default && hasContent(responses.default) ? responses.default : null
110+
const res200 = responses['200'] && typeof responses['200'] === 'object' ? responses['200'] : null
111+
const contentDefault = resDefault?.content?.['application/json']
112+
const content200 = res200 && hasContent(res200) ? res200.content?.['application/json'] : null
113+
const schemaFromContent = (contentDefault && typeof contentDefault === 'object' && 'schema' in contentDefault ? (contentDefault as { schema: unknown }).schema : null)
114+
?? (content200 && typeof content200 === 'object' && 'schema' in content200 ? (content200 as { schema: unknown }).schema : null)
115+
const schemaFromRes200 = res200 && typeof res200 === 'object' && 'schema' in res200 && !('content' in res200) ? (res200 as { schema: unknown }).schema : null
116+
const responseSchema = schemaFromContent ?? schemaFromRes200
117+
const responseType = responseSchema && typeof responseSchema === 'object' ? parseSchemaType(responseSchema as Parameters<typeof parseSchemaType>[0]) : 'void'
109118

110119
if (configRead.config.responseRequired)
111120
deepSignRequired(interfaces.find(v => v.name === responseType)?.properties || [])
@@ -119,5 +128,5 @@ export function parseMethodMetadata({ method, path, responses, options: meta }:
119128
}
120129
}
121130

122-
return { description: comments.filter(Boolean), name, url, responseType, body: [] as string[] }
131+
return { description: comments, name, url, responseType, body: [] as string[] }
123132
}

packages/parser/src/parses/parameter.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,14 @@ export function parseParameterFiled(parameter: Parameter) {
1919
field.description = `@description ${field.description}`
2020

2121
if (parameter.in === 'query' && parameter.type === 'array') {
22-
const enums = spliceEnumDescription(parameter.name, parameter.items?.enum)
23-
field.description = [field.description || '', enums].filter(Boolean)
22+
const rawEnum = parameter.items?.enum
23+
const enums = Array.isArray(rawEnum) ? rawEnum.filter((e): e is string => typeof e === 'string') : []
24+
const enumsDesc = spliceEnumDescription(parameter.name, enums)
25+
field.description = [field.description || '', enumsDesc].filter(Boolean)
2426
}
2527

2628
if (['formData', 'body', 'header', 'path', 'query'].includes(parameter.in))
27-
field.type = parseSchemaType(parameter)
29+
field.type = parseSchemaType(parameter as Parameters<typeof parseSchemaType>[0])
2830

2931
if (!field.description)
3032
delete field.description

packages/parser/src/parses/schema.ts

Lines changed: 32 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,20 @@
11
import type { StatementField } from '@genapi/shared'
2-
import type { Properties, Schema } from 'openapi-specification-types'
2+
import type { Properties, Schema, SchemaType } from 'openapi-specification-types'
33
import { inject } from '@genapi/shared'
44
import { isArray, uniq } from '@hairy/utils'
55
import { spliceEnumType, useRefMap, varName } from '../utils'
66

7-
type SchemaWithAllOf = Schema & { allOf?: Schema[] }
7+
type SchemaWithAllOf = Schema & { allOf?: Schema[], schema?: Schema }
8+
type SchemaLike = SchemaWithAllOf & { required?: boolean | string[] }
9+
10+
function schemaRequired(schema: SchemaLike, field?: string): boolean {
11+
const r = schema.required
12+
if (r === undefined)
13+
return true
14+
if (typeof r === 'boolean')
15+
return r
16+
return field !== undefined ? r.includes(field) : true
17+
}
818

919
/**
1020
* parse schema to type
@@ -56,7 +66,7 @@ export function parseSchemaType(propertie: SchemaWithAllOf): string {
5666
isMerge = true
5767
fields[field] = {
5868
type,
59-
required: item.required ?? true,
69+
required: schemaRequired(item as SchemaLike, field),
6070
description: item.description,
6171
name: field,
6272
}
@@ -77,18 +87,23 @@ export function parseSchemaType(propertie: SchemaWithAllOf): string {
7787
return name
7888
}
7989

80-
if (propertie.schema)
81-
return parseSchemaType(propertie.schema)
90+
const schemaRef = (propertie as SchemaWithAllOf).schema
91+
if (schemaRef && typeof schemaRef === 'object')
92+
return parseSchemaType(schemaRef)
8293

8394
// TODO: handle additionalProperties
84-
if (propertie.additionalProperties)
85-
return `Record<string, ${parseSchemaType(propertie.additionalProperties)}>`
95+
const addProps = propertie.additionalProperties
96+
if (addProps === true)
97+
return 'Record<string, any>'
98+
if (addProps && typeof addProps === 'object')
99+
return `Record<string, ${parseSchemaType(addProps)}>`
86100
if (propertie.type === 'object') {
87101
const fields: Record<string, StatementField> = {}
88102
for (const [field, item] of Object.entries(propertie.properties || {})) {
103+
const itemLike = item as SchemaLike
89104
fields[field] = {
90105
type: parseSchemaType(item),
91-
required: item.required,
106+
required: schemaRequired(itemLike, field),
92107
description: item.description,
93108
name: field,
94109
}
@@ -100,8 +115,11 @@ export function parseSchemaType(propertie: SchemaWithAllOf): string {
100115
return 'any'
101116

102117
if (propertie.type === 'array') {
103-
if (propertie.items?.enum)
104-
return ['string', spliceEnumType(propertie.items.enum)].filter(Boolean).join(' | ')
118+
const itemsEnum = propertie.items?.enum
119+
if (Array.isArray(itemsEnum)) {
120+
const enumStrs = itemsEnum.filter((e): e is string => typeof e === 'string')
121+
return ['string', spliceEnumType(enumStrs)].filter(Boolean).join(' | ')
122+
}
105123

106124
let itemsType = parseSchemaType(propertie.items!)
107125
itemsType = itemsType.includes('|') ? `(${itemsType})` : itemsType
@@ -111,8 +129,10 @@ export function parseSchemaType(propertie: SchemaWithAllOf): string {
111129
if (propertie.type === 'boolean')
112130
return propertie.type
113131

114-
if (isArray(propertie.type))
115-
return uniq(propertie.type.map(type => parseSchemaType({ type }))).join(' | ')
132+
if (isArray(propertie.type)) {
133+
const types = (propertie.type as unknown[]).filter((t): t is string => typeof t === 'string')
134+
return uniq(types.map(type => parseSchemaType({ type: type as SchemaType }))).join(' | ')
135+
}
116136

117137
if (['integer', 'long', 'float', 'byte', 'TypesLong', 'number'].includes(propertie.type))
118138
return 'number'

packages/parser/src/transform/definitions.ts

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,15 +21,13 @@ export function transformDefinitions(definitions: Definitions) {
2121
})
2222

2323
function defToFields(name: string, propertie: Schema) {
24-
propertie.required = definition?.required?.includes(name)
25-
if (propertie.description)
26-
propertie.description = `@description ${propertie.description}`
27-
24+
const required = Array.isArray(definition?.required) ? definition.required.includes(name) : undefined
25+
const description = propertie.description ? `@description ${propertie.description}` : undefined
2826
return {
2927
name: varFiled(name),
3028
type: parseSchemaType(propertie),
31-
description: propertie.description,
32-
required: propertie.required,
29+
description,
30+
required: required ?? (typeof propertie.required === 'boolean' ? propertie.required : undefined),
3331
}
3432
}
3533
}

packages/parser/src/traverse/paths.ts

Lines changed: 33 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,15 @@
11
import type { Method, Parameter, Paths, RequestBody, Responses } from 'openapi-specification-types'
22

3+
const HTTP_METHODS = ['get', 'put', 'post', 'delete', 'patch', 'head', 'options'] as const
4+
5+
function isParameter(p: Parameter | { $ref?: string }): p is Parameter {
6+
return p != null && 'name' in p && 'in' in p
7+
}
8+
9+
function isOperationObject(value: unknown): value is Method {
10+
return value != null && typeof value === 'object' && 'responses' in value
11+
}
12+
313
export interface PathMethod {
414
path: string
515
parameters: Parameter[]
@@ -17,19 +27,23 @@ export interface PathMethod {
1727
*/
1828
export function traversePaths(paths: Paths, callback: (options: PathMethod) => void) {
1929
for (const [path, _others] of Object.entries(paths)) {
20-
let { parameters = [], ...methods } = _others
21-
for (const method in methods) {
22-
const options = methods[method as keyof typeof methods]
30+
if (typeof _others === 'string' || Array.isArray(_others))
31+
continue
32+
const pathParameters = (Array.isArray(_others.parameters) ? _others.parameters : [])
33+
.filter(isParameter)
34+
for (const method of HTTP_METHODS) {
35+
const options = _others[method]
36+
if (!isOperationObject(options))
37+
continue
2338
const parametersMap = new Map<string, Parameter>()
24-
25-
for (const parameter of parameters)
39+
for (const parameter of pathParameters)
2640
parametersMap.set(parameter.name, parameter)
27-
for (const parameter of (options.parameters || []))
41+
const opParams = (options.parameters ?? []).filter(isParameter)
42+
for (const parameter of opParams)
2843
parametersMap.set(parameter.name, parameter)
29-
30-
parameters = [...parametersMap.values()]
31-
32-
extendsRequestBody(parameters, options.requestBody)
44+
const parameters = [...parametersMap.values()]
45+
if (options.requestBody && 'content' in options.requestBody)
46+
extendsRequestBody(parameters, options.requestBody)
3347
callback({
3448
responses: options.responses,
3549
path,
@@ -42,24 +56,25 @@ export function traversePaths(paths: Paths, callback: (options: PathMethod) => v
4256
}
4357

4458
function extendsRequestBody(parameters: Parameter[], requestBody?: RequestBody) {
45-
if (!requestBody)
59+
if (!requestBody?.content)
4660
return
47-
if (requestBody.content['multipart/form-data']) {
48-
const properties = requestBody.content['multipart/form-data'].schema.properties!
49-
for (const name in Object.keys(properties)) {
61+
const multipart = requestBody.content['multipart/form-data']
62+
if (multipart && 'schema' in multipart && multipart.schema?.properties) {
63+
const properties = multipart.schema.properties
64+
for (const name of Object.keys(properties)) {
5065
parameters.push({
5166
in: 'formData',
5267
name,
5368
description: requestBody.description,
54-
...properties[name],
69+
...(properties[name] as object),
5570
})
5671
}
5772
return
5873
}
59-
60-
if (requestBody.content['application/json']) {
74+
const json = requestBody.content['application/json']
75+
if (json && typeof json === 'object') {
6176
parameters.push({
62-
...requestBody.content['application/json'] as any,
77+
...(json as object),
6378
description: requestBody.description,
6479
in: 'body',
6580
name: 'body',

0 commit comments

Comments
 (0)