From 386378cf4fc2459a4f452a9f9cce902bea448f4c Mon Sep 17 00:00:00 2001 From: luochao <1055120207@qq.com> Date: Thu, 18 Sep 2025 11:30:11 +0800 Subject: [PATCH] fix: fix parse parameters ref error #486 --- .changeset/tricky-pants-remain.md | 5 + src/generator/serviceGenarator.ts | 63 ++++++--- ...32\204 $ref \345\274\225\347\224\250.snap" | 92 +++++++++++++ test/common.spec.ts | 13 ++ .../openapi-components-parameters.yaml | 128 ++++++++++++++++++ 5 files changed, 284 insertions(+), 17 deletions(-) create mode 100644 .changeset/tricky-pants-remain.md create mode 100644 "test/__snapshots__/common/\346\265\213\350\257\225\350\247\243\346\236\220 components.parameters \344\270\255\347\232\204 $ref \345\274\225\347\224\250.snap" create mode 100644 test/example-files/openapi-components-parameters.yaml diff --git a/.changeset/tricky-pants-remain.md b/.changeset/tricky-pants-remain.md new file mode 100644 index 0000000..5e489e1 --- /dev/null +++ b/.changeset/tricky-pants-remain.md @@ -0,0 +1,5 @@ +--- +'openapi-ts-request': patch +--- + +fix: fix parse parameters ref error #486 diff --git a/src/generator/serviceGenarator.ts b/src/generator/serviceGenarator.ts index 27efd55..4a49149 100644 --- a/src/generator/serviceGenarator.ts +++ b/src/generator/serviceGenarator.ts @@ -501,28 +501,41 @@ export default class ServiceGenerator { markAllowedSchema(JSON.stringify(pathItem), this.openAPIData); operationObject.parameters = operationObject.parameters?.filter( - (item: ParameterObject) => item?.in !== `${parametersInsEnum.header}` + (item: ParameterObject | ReferenceObject) => { + const parameter = this.resolveParameterRef(item); + return parameter?.in !== `${parametersInsEnum.header}`; + } ); const props = [] as IPropObject[]; - operationObject.parameters?.forEach((parameter: ParameterObject) => { - props.push({ - name: parameter.name, - desc: (parameter.description ?? '').replace(lineBreakReg, ''), - required: parameter.required || false, - type: this.getType(parameter.schema), - }); - }); + operationObject.parameters?.forEach( + (param: ParameterObject | ReferenceObject) => { + const parameter = this.resolveParameterRef(param); + if (parameter) { + props.push({ + name: parameter.name, + desc: (parameter.description ?? '').replace(lineBreakReg, ''), + required: parameter.required || false, + type: this.getType(parameter.schema), + }); + } + } + ); // parameters may be in path - pathItem.parameters?.forEach((parameter: ParameterObject) => { - props.push({ - name: parameter.name, - desc: (parameter.description ?? '').replace(lineBreakReg, ''), - required: parameter.required, - type: this.getType(parameter.schema), - }); - }); + pathItem.parameters?.forEach( + (param: ParameterObject | ReferenceObject) => { + const parameter = this.resolveParameterRef(param); + if (parameter) { + props.push({ + name: parameter.name, + desc: (parameter.description ?? '').replace(lineBreakReg, ''), + required: parameter.required, + type: this.getType(parameter.schema), + }); + } + } + ); const typeName = this.getFunctionParamsTypeName({ ...operationObject, @@ -1468,6 +1481,22 @@ export default class ServiceGenerator { }); } + private resolveParameterRef( + param: ParameterObject | ReferenceObject + ): ParameterObject | null { + if (!isReferenceObject(param)) { + return param; + } + + // 解析 $ref 引用,从 components.parameters 中获取实际定义 + const refName = getLastRefName(param.$ref); + const parameter = this.openAPIData.components?.parameters?.[ + refName + ] as ParameterObject; + + return parameter || null; + } + private resolveRefObject(refObject: ReferenceObject | T): T { if (!isReferenceObject(refObject)) { return refObject; diff --git "a/test/__snapshots__/common/\346\265\213\350\257\225\350\247\243\346\236\220 components.parameters \344\270\255\347\232\204 $ref \345\274\225\347\224\250.snap" "b/test/__snapshots__/common/\346\265\213\350\257\225\350\247\243\346\236\220 components.parameters \344\270\255\347\232\204 $ref \345\274\225\347\224\250.snap" new file mode 100644 index 0000000..496bd2e --- /dev/null +++ "b/test/__snapshots__/common/\346\265\213\350\257\225\350\247\243\346\236\220 components.parameters \344\270\255\347\232\204 $ref \345\274\225\347\224\250.snap" @@ -0,0 +1,92 @@ +/* eslint-disable */ +// @ts-ignore +export * from './types'; + +export * from './products'; +/* eslint-disable */ +// @ts-ignore +import { request } from 'axios'; + +import * as API from './types'; + +/** Get product list Get paginated product list with search, filter, and sort support GET /products */ +export function productsUsingGet({ + params, + options, +}: { + // 叠加生成的Param类型 (非body参数openapi默认没有生成对象) + params: API.ProductsUsingGetParams; + options?: { [key: string]: unknown }; +}) { + return request< + API.BaseResponse & { + data?: API.ProductListResponse; + } + >('/products', { + method: 'GET', + params: { + // page has a default value: 1 + page: '1', + // pageSize has a default value: 10 + pageSize: '10', + + ...params, + }, + ...(options || {}), + }); +} +/* eslint-disable */ +// @ts-ignore + +export type BaseResponse = { + /** Response status code */ + code?: number; + /** Response message */ + message?: string; + /** Response data */ + data?: Record; +}; + +export type PaginatedResponse = { + /** Data list */ + list?: Record[]; + /** Total records */ + total?: number; + /** Current page */ + page?: number; +}; + +export type Product = { + /** Product ID */ + productId?: string; + /** Product name */ + name?: string; + /** Product description */ + description?: string; +}; + +export type ProductListResponse = + // #/components/schemas/PaginatedResponse + PaginatedResponse & { + list?: Product[]; + }; + +export type ProductsUsingGetParams = { + /** Page number */ + page?: number; + /** Page size */ + pageSize?: number; + /** Search keyword */ + keyword?: string; + /** Product category ID */ + categoryId?: string; +}; + +export type ProductsUsingGetResponses = { + /** + * Get successful + */ + 200: BaseResponse & { + data?: ProductListResponse; + }; +}; diff --git a/test/common.spec.ts b/test/common.spec.ts index ac37850..bb9f2e1 100644 --- a/test/common.spec.ts +++ b/test/common.spec.ts @@ -484,4 +484,17 @@ export async function ${api.functionName}(${api.body ? `data: ${api.body.type}` readGeneratedFiles('./apis/openapi-anonymous-response') ).resolves.toMatchFileSnapshot(getSnapshotDir(ctx)); }); + + it('测试解析 components.parameters 中的 $ref 引用', async (ctx) => { + await openAPI.generateService({ + schemaPath: join( + import.meta.dirname, + './example-files/openapi-components-parameters.yaml' + ), + serversPath: './apis/components-parameters', + }); + await expect( + readGeneratedFiles('./apis/components-parameters') + ).resolves.toMatchFileSnapshot(getSnapshotDir(ctx)); + }); }); diff --git a/test/example-files/openapi-components-parameters.yaml b/test/example-files/openapi-components-parameters.yaml new file mode 100644 index 0000000..7dd2735 --- /dev/null +++ b/test/example-files/openapi-components-parameters.yaml @@ -0,0 +1,128 @@ +openapi: 3.0.3 +servers: + - url: http://localhost:3000/v1 + description: 开发环境 + +security: + - BearerAuth: [] + +paths: + /products: + get: + tags: + - Products + summary: Get product list + description: Get paginated product list with search, filter, and sort support + parameters: + - $ref: '#/components/parameters/PageParam' + - $ref: '#/components/parameters/PageSizeParam' + - $ref: '#/components/parameters/KeywordParam' + - name: categoryId + in: query + description: Product category ID + schema: + type: string + example: cat_001 + responses: + '200': + description: Get successful + content: + application/json: + schema: + allOf: + - $ref: '#/components/schemas/BaseResponse' + - type: object + properties: + data: + $ref: '#/components/schemas/ProductListResponse' + +components: + schemas: + # Base response format + BaseResponse: + type: object + properties: + code: + type: integer + description: Response status code + example: 200 + message: + type: string + description: Response message + example: Operation successful + data: + type: object + description: Response data + + ProductListResponse: + allOf: + - $ref: '#/components/schemas/PaginatedResponse' + - type: object + properties: + list: + type: array + items: + $ref: '#/components/schemas/Product' + + PaginatedResponse: + type: object + properties: + list: + type: array + description: Data list + items: + type: object + total: + type: integer + description: Total records + example: 100 + page: + type: integer + description: Current page + example: 1 + + Product: + type: object + properties: + productId: + type: string + description: Product ID + example: prod_001 + name: + type: string + description: Product name + example: Classic Pearl Milk Tea + description: + type: string + description: Product description + example: 'Rich milk tea with chewy pearls, a classic that never goes out of style' + + parameters: + # Pagination parameters + PageParam: + name: page + in: query + description: Page number + schema: + type: integer + minimum: 1 + default: 1 + example: 1 + + PageSizeParam: + name: pageSize + in: query + description: Page size + schema: + type: integer + minimum: 1 + maximum: 100 + default: 10 + example: 10 + KeywordParam: + name: keyword + in: query + description: Search keyword + schema: + type: string + example: Pearl Milk Tea \ No newline at end of file