fork of hey-api/openapi-ts because I need some additional things
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Merge pull request #3411 from hey-api/copilot/add-patch-spec-callback

authored by

Lubos and committed by
GitHub
37026a14 5ae69ba6

+662 -159
+6
.changeset/heavy-hounds-listen.md
··· 1 + --- 2 + "@hey-api/openapi-ts": patch 3 + "@hey-api/shared": patch 4 + --- 5 + 6 + **parser**: add `patch.input` and shorthand `patch()` option for full specification transformations
+1 -1
packages/openapi-python/src/createClient.ts
··· 100 100 } 101 101 102 102 const eventInputPatch = logger.timeEvent('input.patch'); 103 - patchOpenApiSpec({ patchOptions: config.parser.patch, spec: data }); 103 + await patchOpenApiSpec({ patchOptions: config.parser.patch, spec: data }); 104 104 eventInputPatch.timeEnd(); 105 105 106 106 const eventParser = logger.timeEvent('parser');
+1 -1
packages/openapi-ts/src/createClient.ts
··· 100 100 } 101 101 102 102 const eventInputPatch = logger.timeEvent('input.patch'); 103 - patchOpenApiSpec({ patchOptions: config.parser.patch, spec: data }); 103 + await patchOpenApiSpec({ patchOptions: config.parser.patch, spec: data }); 104 104 eventInputPatch.timeEnd(); 105 105 106 106 const eventParser = logger.timeEvent('parser');
+160 -116
packages/shared/src/config/parser/patch.ts
··· 1 1 import type { MaybeFunc } from '@hey-api/types'; 2 2 3 3 import type { 4 + OpenApi, 4 5 OpenApiMetaObject, 5 6 OpenApiOperationObject, 6 7 OpenApiParameterObject, ··· 9 10 OpenApiSchemaObject, 10 11 } from '../../openApi/types'; 11 12 12 - export type Patch = { 13 - /** 14 - * Patch the OpenAPI meta object in place. Useful for modifying general metadata such as title, description, version, or custom fields before further processing. 15 - * 16 - * @param meta The OpenAPI meta object for the current version. 17 - */ 18 - meta?: ( 19 - meta: OpenApiMetaObject.V2_0_X | OpenApiMetaObject.V3_0_X | OpenApiMetaObject.V3_1_X, 20 - ) => void; 21 - /** 22 - * Patch OpenAPI operations in place. The key is the operation method and operation path, and the function receives the operation object to modify directly. 23 - * 24 - * @example 25 - * operations: { 26 - * 'GET /foo': (operation) => { 27 - * operation.responses['200'].description = 'foo'; 28 - * } 29 - * } 30 - */ 31 - operations?: Record< 32 - string, 33 - ( 34 - operation: 35 - | OpenApiOperationObject.V2_0_X 36 - | OpenApiOperationObject.V3_0_X 37 - | OpenApiOperationObject.V3_1_X, 38 - ) => void 39 - >; 40 - /** 41 - * Patch OpenAPI parameters in place. The key is the parameter name, and the function receives the parameter object to modify directly. 42 - * 43 - * @example 44 - * parameters: { 45 - * limit: (parameter) => { 46 - * parameter.schema.type = 'integer'; 47 - * } 48 - * } 49 - */ 50 - parameters?: Record< 51 - string, 52 - (parameter: OpenApiParameterObject.V3_0_X | OpenApiParameterObject.V3_1_X) => void 53 - >; 54 - /** 55 - * Patch OpenAPI request bodies in place. The key is the request body name, and the function receives the request body object to modify directly. 56 - * 57 - * @example 58 - * requestBodies: { 59 - * CreateUserRequest: (requestBody) => { 60 - * requestBody.required = true; 61 - * } 62 - * } 63 - */ 64 - requestBodies?: Record< 65 - string, 66 - (requestBody: OpenApiRequestBodyObject.V3_0_X | OpenApiRequestBodyObject.V3_1_X) => void 67 - >; 68 - /** 69 - * Patch OpenAPI responses in place. The key is the response name, and the function receives the response object to modify directly. 70 - * 71 - * @example 72 - * responses: { 73 - * NotFound: (response) => { 74 - * response.description = 'Resource not found.'; 75 - * } 76 - * } 77 - */ 78 - responses?: Record< 79 - string, 80 - (response: OpenApiResponseObject.V3_0_X | OpenApiResponseObject.V3_1_X) => void 81 - >; 82 - /** 83 - * Each function receives the schema object to be modified in place. Common 84 - * use cases include fixing incorrect data types, removing unwanted 85 - * properties, adding missing fields, or standardizing date/time formats. 86 - * 87 - * @example 88 - * ```js 89 - * schemas: { 90 - * Foo: (schema) => { 91 - * // convert date-time format to timestamp 92 - * delete schema.properties.updatedAt.format; 93 - * schema.properties.updatedAt.type = 'number'; 94 - * }, 95 - * Bar: (schema) => { 96 - * // add missing property 97 - * schema.properties.metadata = { 98 - * additionalProperties: true, 99 - * type: 'object', 100 - * }; 101 - * schema.required = ['metadata']; 102 - * }, 103 - * Baz: (schema) => { 104 - * // remove property 105 - * delete schema.properties.internalField; 106 - * } 107 - * } 108 - * ``` 109 - */ 110 - schemas?: Record< 111 - string, 112 - ( 113 - schema: OpenApiSchemaObject.V2_0_X | OpenApiSchemaObject.V3_0_X | OpenApiSchemaObject.V3_1_X, 114 - ) => void 115 - >; 116 - /** 117 - * Patch the OpenAPI version string. The function receives the current version and should return the new version string. 118 - * Useful for normalizing or overriding the version value before further processing. 119 - * 120 - * @param version The current OpenAPI version string. 121 - * @returns The new version string to use. 122 - * 123 - * @example 124 - * version: (version) => version.replace(/^v/, '') 125 - */ 126 - version?: MaybeFunc<(version: string) => string>; 127 - }; 13 + export type PatchInputFn = ( 14 + spec: OpenApi.V2_0_X | OpenApi.V3_0_X | OpenApi.V3_1_X, 15 + ) => void | Promise<void>; 16 + 17 + export type Patch = 18 + | PatchInputFn 19 + | { 20 + /** 21 + * Patch the raw OpenAPI spec object in place. Called before all other 22 + * patch callbacks. Useful for bulk/structural transformations such as 23 + * adding new component definitions or modifying many operations at once. 24 + * 25 + * @param spec The OpenAPI spec object for the current version. 26 + * 27 + * @example 28 + * ```ts 29 + * input: (spec) => { 30 + * // Create new component parameters 31 + * if (!spec.components) spec.components = {}; 32 + * if (!spec.components.parameters) spec.components.parameters = {}; 33 + * spec.components.parameters.MyParam = { 34 + * in: 'query', 35 + * name: 'myParam', 36 + * schema: { type: 'string' } 37 + * }; 38 + * 39 + * // Inject parameters into operations 40 + * for (const [path, pathItem] of Object.entries(spec.paths ?? {})) { 41 + * if (pathItem?.get) { 42 + * if (!Array.isArray(pathItem.get.parameters)) { 43 + * pathItem.get.parameters = []; 44 + * } 45 + * pathItem.get.parameters.push({ 46 + * $ref: '#/components/parameters/MyParam' 47 + * }); 48 + * } 49 + * } 50 + * } 51 + * ``` 52 + */ 53 + input?: PatchInputFn; 54 + /** 55 + * Patch the OpenAPI meta object in place. Useful for modifying general metadata such as title, description, version, or custom fields before further processing. 56 + * 57 + * @param meta The OpenAPI meta object for the current version. 58 + */ 59 + meta?: ( 60 + meta: OpenApiMetaObject.V2_0_X | OpenApiMetaObject.V3_0_X | OpenApiMetaObject.V3_1_X, 61 + ) => void; 62 + /** 63 + * Patch OpenAPI operations in place. The key is the operation method and operation path, and the function receives the operation object to modify directly. 64 + * 65 + * @example 66 + * operations: { 67 + * 'GET /foo': (operation) => { 68 + * operation.responses['200'].description = 'foo'; 69 + * } 70 + * } 71 + */ 72 + operations?: Record< 73 + string, 74 + ( 75 + operation: 76 + | OpenApiOperationObject.V2_0_X 77 + | OpenApiOperationObject.V3_0_X 78 + | OpenApiOperationObject.V3_1_X, 79 + ) => void 80 + >; 81 + /** 82 + * Patch OpenAPI parameters in place. The key is the parameter name, and the function receives the parameter object to modify directly. 83 + * 84 + * @example 85 + * parameters: { 86 + * limit: (parameter) => { 87 + * parameter.schema.type = 'integer'; 88 + * } 89 + * } 90 + */ 91 + parameters?: Record< 92 + string, 93 + (parameter: OpenApiParameterObject.V3_0_X | OpenApiParameterObject.V3_1_X) => void 94 + >; 95 + /** 96 + * Patch OpenAPI request bodies in place. The key is the request body name, and the function receives the request body object to modify directly. 97 + * 98 + * @example 99 + * requestBodies: { 100 + * CreateUserRequest: (requestBody) => { 101 + * requestBody.required = true; 102 + * } 103 + * } 104 + */ 105 + requestBodies?: Record< 106 + string, 107 + (requestBody: OpenApiRequestBodyObject.V3_0_X | OpenApiRequestBodyObject.V3_1_X) => void 108 + >; 109 + /** 110 + * Patch OpenAPI responses in place. The key is the response name, and the function receives the response object to modify directly. 111 + * 112 + * @example 113 + * responses: { 114 + * NotFound: (response) => { 115 + * response.description = 'Resource not found.'; 116 + * } 117 + * } 118 + */ 119 + responses?: Record< 120 + string, 121 + (response: OpenApiResponseObject.V3_0_X | OpenApiResponseObject.V3_1_X) => void 122 + >; 123 + /** 124 + * Each function receives the schema object to be modified in place. Common 125 + * use cases include fixing incorrect data types, removing unwanted 126 + * properties, adding missing fields, or standardizing date/time formats. 127 + * 128 + * @example 129 + * ```js 130 + * schemas: { 131 + * Foo: (schema) => { 132 + * // convert date-time format to timestamp 133 + * delete schema.properties.updatedAt.format; 134 + * schema.properties.updatedAt.type = 'number'; 135 + * }, 136 + * Bar: (schema) => { 137 + * // add missing property 138 + * schema.properties.metadata = { 139 + * additionalProperties: true, 140 + * type: 'object', 141 + * }; 142 + * schema.required = ['metadata']; 143 + * }, 144 + * Baz: (schema) => { 145 + * // remove property 146 + * delete schema.properties.internalField; 147 + * } 148 + * } 149 + * ``` 150 + */ 151 + schemas?: Record< 152 + string, 153 + ( 154 + schema: 155 + | OpenApiSchemaObject.V2_0_X 156 + | OpenApiSchemaObject.V3_0_X 157 + | OpenApiSchemaObject.V3_1_X, 158 + ) => void 159 + >; 160 + /** 161 + * Patch the OpenAPI version string. The function receives the current version and should return the new version string. 162 + * Useful for normalizing or overriding the version value before further processing. 163 + * 164 + * @param version The current OpenAPI version string. 165 + * @returns The new version string to use. 166 + * 167 + * @example 168 + * version: (version) => version.replace(/^v/, '') 169 + */ 170 + version?: MaybeFunc<(version: string) => string>; 171 + };
+484 -40
packages/shared/src/openApi/shared/utils/__tests__/patch.test.ts
··· 19 19 }; 20 20 21 21 describe('patchOpenApiSpec', () => { 22 + describe('patch.input', () => { 23 + describe('OpenAPI v3', () => { 24 + it('calls patch.input function before other patches', async () => { 25 + const inputFn = vi.fn(); 26 + const metaFn = vi.fn(); 27 + const spec: OpenApi.V3_1_X = { 28 + ...specMetadataV3, 29 + }; 30 + 31 + await patchOpenApiSpec({ 32 + patchOptions: { 33 + input: inputFn, 34 + meta: metaFn, 35 + }, 36 + spec, 37 + }); 38 + 39 + // Both should be called 40 + expect(inputFn).toHaveBeenCalledOnce(); 41 + expect(inputFn).toHaveBeenCalledWith(spec); 42 + expect(metaFn).toHaveBeenCalledOnce(); 43 + }); 44 + 45 + it('allows bulk creation of component parameters', async () => { 46 + const spec: OpenApi.V3_1_X = { 47 + ...specMetadataV3, 48 + paths: {}, 49 + }; 50 + 51 + await patchOpenApiSpec({ 52 + patchOptions: { 53 + input: (spec) => { 54 + if ('openapi' in spec) { 55 + if (!spec.components) spec.components = {}; 56 + if (!spec.components.parameters) spec.components.parameters = {}; 57 + spec.components.parameters.MyParam = { 58 + in: 'query', 59 + name: 'myParam', 60 + schema: { type: 'string' }, 61 + } as any; 62 + } 63 + }, 64 + }, 65 + spec, 66 + }); 67 + 68 + expect(spec.components?.parameters?.MyParam).toEqual({ 69 + in: 'query', 70 + name: 'myParam', 71 + schema: { type: 'string' }, 72 + }); 73 + }); 74 + 75 + it('allows injecting parameters into multiple operations', async () => { 76 + const spec: OpenApi.V3_1_X = { 77 + ...specMetadataV3, 78 + components: { 79 + parameters: { 80 + SharedParam: { 81 + in: 'query', 82 + name: 'shared', 83 + schema: { type: 'string' }, 84 + } as any, 85 + }, 86 + }, 87 + paths: { 88 + '/bar': { 89 + get: { 90 + responses: {}, 91 + }, 92 + }, 93 + '/baz': { 94 + post: { 95 + responses: {}, 96 + }, 97 + }, 98 + '/foo': { 99 + get: { 100 + responses: {}, 101 + }, 102 + }, 103 + } as any, 104 + }; 105 + 106 + await patchOpenApiSpec({ 107 + patchOptions: { 108 + input: (spec) => { 109 + // Inject parameter into all GET operations 110 + for (const [, pathItem] of Object.entries(spec.paths ?? {})) { 111 + if (pathItem?.get) { 112 + if (!Array.isArray(pathItem.get.parameters)) { 113 + pathItem.get.parameters = []; 114 + } 115 + (pathItem.get.parameters as any[]).push({ 116 + $ref: '#/components/parameters/SharedParam', 117 + }); 118 + } 119 + } 120 + }, 121 + }, 122 + spec, 123 + }); 124 + 125 + expect((spec.paths as any)['/foo'].get.parameters).toEqual([ 126 + { $ref: '#/components/parameters/SharedParam' }, 127 + ]); 128 + expect((spec.paths as any)['/bar'].get.parameters).toEqual([ 129 + { $ref: '#/components/parameters/SharedParam' }, 130 + ]); 131 + expect((spec.paths as any)['/baz'].post.parameters).toBeUndefined(); 132 + }); 133 + 134 + it('allows complex Redfish-like transformations', async () => { 135 + const spec: OpenApi.V3_1_X = { 136 + ...specMetadataV3, 137 + paths: { 138 + '/other/path': { 139 + get: { 140 + responses: {}, 141 + }, 142 + }, 143 + '/redfish/v1/Chassis': { 144 + get: { 145 + responses: {}, 146 + }, 147 + }, 148 + '/redfish/v1/Systems': { 149 + get: { 150 + responses: {}, 151 + }, 152 + }, 153 + } as any, 154 + }; 155 + 156 + const QUERY_PARAMS = [ 157 + { description: 'Expand related resources.', key: '$expand' }, 158 + { description: 'Select subset.', key: '$select' }, 159 + ]; 160 + 161 + await patchOpenApiSpec({ 162 + patchOptions: { 163 + input: (spec) => { 164 + if (!('openapi' in spec)) return; 165 + 166 + // 1. Create component parameters 167 + if (!spec.components) spec.components = {}; 168 + if (!spec.components.parameters) spec.components.parameters = {}; 169 + 170 + for (const param of QUERY_PARAMS) { 171 + (spec.components.parameters as any)[`Redfish_${param.key}`] = { 172 + description: param.description, 173 + in: 'query', 174 + name: param.key, 175 + required: false, 176 + schema: { type: 'string' }, 177 + }; 178 + } 179 + 180 + // 2. Inject into Redfish paths 181 + for (const [path, pathItem] of Object.entries(spec.paths ?? {})) { 182 + if (!path.startsWith('/redfish/v1')) continue; 183 + const getOp = pathItem?.get; 184 + if (!getOp) continue; 185 + 186 + if (!Array.isArray(getOp.parameters)) getOp.parameters = []; 187 + for (const param of QUERY_PARAMS) { 188 + (getOp.parameters as any[]).push({ 189 + $ref: `#/components/parameters/Redfish_${param.key}`, 190 + }); 191 + } 192 + } 193 + }, 194 + }, 195 + spec, 196 + }); 197 + 198 + // Verify component parameters were created 199 + expect(spec.components?.parameters).toHaveProperty('Redfish_$expand'); 200 + expect(spec.components?.parameters).toHaveProperty('Redfish_$select'); 201 + 202 + // Verify they were injected into Redfish paths 203 + expect((spec.paths as any)['/redfish/v1/Systems'].get.parameters).toHaveLength(2); 204 + expect((spec.paths as any)['/redfish/v1/Chassis'].get.parameters).toHaveLength(2); 205 + 206 + // Verify they were NOT injected into non-Redfish paths 207 + expect((spec.paths as any)['/other/path'].get.parameters).toBeUndefined(); 208 + }); 209 + }); 210 + 211 + describe('OpenAPI v2', () => { 212 + it('calls patch.input function for v2 specs', async () => { 213 + const inputFn = vi.fn(); 214 + const spec: OpenApi.V2_0_X = { 215 + ...specMetadataV2, 216 + }; 217 + 218 + await patchOpenApiSpec({ 219 + patchOptions: { 220 + input: inputFn, 221 + }, 222 + spec, 223 + }); 224 + 225 + expect(inputFn).toHaveBeenCalledOnce(); 226 + expect(inputFn).toHaveBeenCalledWith(spec); 227 + }); 228 + 229 + it('allows adding definitions in v2 specs', async () => { 230 + const spec: OpenApi.V2_0_X = { 231 + ...specMetadataV2, 232 + }; 233 + 234 + await patchOpenApiSpec({ 235 + patchOptions: { 236 + input: (spec) => { 237 + if ('swagger' in spec) { 238 + if (!spec.definitions) spec.definitions = {}; 239 + spec.definitions.NewSchema = { 240 + properties: { 241 + id: { type: 'string' }, 242 + }, 243 + type: 'object', 244 + } as any; 245 + } 246 + }, 247 + }, 248 + spec, 249 + }); 250 + 251 + expect(spec.definitions?.NewSchema).toEqual({ 252 + properties: { 253 + id: { type: 'string' }, 254 + }, 255 + type: 'object', 256 + }); 257 + }); 258 + }); 259 + }); 260 + 261 + describe('async patch support', () => { 262 + describe('patch.input async', () => { 263 + it('supports async patch.input function', async () => { 264 + const spec: OpenApi.V3_1_X = { 265 + ...specMetadataV3, 266 + }; 267 + 268 + let asyncExecuted = false; 269 + 270 + await patchOpenApiSpec({ 271 + patchOptions: { 272 + input: async (spec) => { 273 + await new Promise((resolve) => setTimeout(resolve, 10)); 274 + spec.info.title = 'Async Modified'; 275 + asyncExecuted = true; 276 + }, 277 + }, 278 + spec, 279 + }); 280 + 281 + expect(asyncExecuted).toBe(true); 282 + expect(spec.info.title).toBe('Async Modified'); 283 + }); 284 + 285 + it('supports async operations in patch.input', async () => { 286 + const spec: OpenApi.V3_1_X = { 287 + ...specMetadataV3, 288 + paths: {}, 289 + }; 290 + 291 + await patchOpenApiSpec({ 292 + patchOptions: { 293 + input: async (spec) => { 294 + // Simulate async operation like fetching data 295 + await new Promise((resolve) => setTimeout(resolve, 5)); 296 + if ('openapi' in spec) { 297 + if (!spec.components) spec.components = {}; 298 + if (!spec.components.parameters) spec.components.parameters = {}; 299 + spec.components.parameters.AsyncParam = { 300 + in: 'query', 301 + name: 'asyncParam', 302 + schema: { type: 'string' }, 303 + } as any; 304 + } 305 + }, 306 + }, 307 + spec, 308 + }); 309 + 310 + expect(spec.components?.parameters?.AsyncParam).toEqual({ 311 + in: 'query', 312 + name: 'asyncParam', 313 + schema: { type: 'string' }, 314 + }); 315 + }); 316 + }); 317 + 318 + describe('shorthand async', () => { 319 + it('supports async shorthand patch function', async () => { 320 + const spec: OpenApi.V3_1_X = { 321 + ...specMetadataV3, 322 + }; 323 + 324 + let asyncExecuted = false; 325 + 326 + await patchOpenApiSpec({ 327 + patchOptions: async (spec) => { 328 + await new Promise((resolve) => setTimeout(resolve, 10)); 329 + spec.info.title = 'Async Shorthand Modified'; 330 + asyncExecuted = true; 331 + }, 332 + spec, 333 + }); 334 + 335 + expect(asyncExecuted).toBe(true); 336 + expect(spec.info.title).toBe('Async Shorthand Modified'); 337 + }); 338 + 339 + it('supports async operations in shorthand function', async () => { 340 + const spec: OpenApi.V3_1_X = { 341 + ...specMetadataV3, 342 + components: { 343 + schemas: { 344 + Foo: { 345 + type: 'string', 346 + }, 347 + }, 348 + }, 349 + }; 350 + 351 + await patchOpenApiSpec({ 352 + patchOptions: async (spec) => { 353 + // Simulate async operation 354 + await new Promise((resolve) => setTimeout(resolve, 5)); 355 + spec.info.description = 'Added via async shorthand'; 356 + if ('openapi' in spec && spec.components?.schemas) { 357 + (spec.components.schemas as any).Bar = { 358 + type: 'number', 359 + }; 360 + } 361 + }, 362 + spec, 363 + }); 364 + 365 + expect(spec.info.description).toBe('Added via async shorthand'); 366 + expect((spec.components?.schemas as any)?.Bar).toEqual({ 367 + type: 'number', 368 + }); 369 + }); 370 + }); 371 + }); 372 + 373 + describe('shorthand patch function', () => { 374 + describe('OpenAPI v3', () => { 375 + it('calls shorthand patch function', async () => { 376 + const patchFn = vi.fn(); 377 + const spec: OpenApi.V3_1_X = { 378 + ...specMetadataV3, 379 + }; 380 + 381 + await patchOpenApiSpec({ 382 + patchOptions: patchFn, 383 + spec, 384 + }); 385 + 386 + expect(patchFn).toHaveBeenCalledOnce(); 387 + expect(patchFn).toHaveBeenCalledWith(spec); 388 + }); 389 + 390 + it('allows modifications through shorthand function', async () => { 391 + const spec: OpenApi.V3_1_X = { 392 + ...specMetadataV3, 393 + }; 394 + 395 + await patchOpenApiSpec({ 396 + patchOptions: (spec) => { 397 + spec.info.title = 'Modified Title'; 398 + }, 399 + spec, 400 + }); 401 + 402 + expect(spec.info.title).toBe('Modified Title'); 403 + }); 404 + 405 + it('shorthand function replaces object-based patch configuration', async () => { 406 + const spec: OpenApi.V3_1_X = { 407 + ...specMetadataV3, 408 + components: { 409 + schemas: { 410 + Foo: { 411 + type: 'string', 412 + }, 413 + }, 414 + }, 415 + }; 416 + 417 + // When using shorthand syntax, only the function is called 418 + // Object properties like meta or schemas would be ignored 419 + await patchOpenApiSpec({ 420 + patchOptions: (spec) => { 421 + spec.info.title = 'Shorthand Title'; 422 + // This is the only code that runs 423 + }, 424 + spec, 425 + }); 426 + 427 + expect(spec.info.title).toBe('Shorthand Title'); 428 + // Schemas remain untouched since no schema patch was applied 429 + expect(spec.components?.schemas?.Foo).toEqual({ type: 'string' }); 430 + }); 431 + }); 432 + 433 + describe('OpenAPI v2', () => { 434 + it('calls shorthand patch function for v2 specs', async () => { 435 + const patchFn = vi.fn(); 436 + const spec: OpenApi.V2_0_X = { 437 + ...specMetadataV2, 438 + }; 439 + 440 + await patchOpenApiSpec({ 441 + patchOptions: patchFn, 442 + spec, 443 + }); 444 + 445 + expect(patchFn).toHaveBeenCalledOnce(); 446 + expect(patchFn).toHaveBeenCalledWith(spec); 447 + }); 448 + 449 + it('allows modifications through shorthand function in v2', async () => { 450 + const spec: OpenApi.V2_0_X = { 451 + ...specMetadataV2, 452 + }; 453 + 454 + await patchOpenApiSpec({ 455 + patchOptions: (spec) => { 456 + spec.info.title = 'Modified V2 Title'; 457 + }, 458 + spec, 459 + }); 460 + 461 + expect(spec.info.title).toBe('Modified V2 Title'); 462 + }); 463 + }); 464 + }); 465 + 22 466 describe('edge cases', () => { 23 - it('does not modify spec', () => { 467 + it('does not modify spec', async () => { 24 468 const spec: OpenApi.V3_1_X = { 25 469 ...specMetadataV3, 26 470 components: { ··· 32 476 }, 33 477 }; 34 478 35 - patchOpenApiSpec({ 479 + await patchOpenApiSpec({ 36 480 patchOptions: undefined, 37 481 spec, 38 482 }); ··· 49 493 }); 50 494 }); 51 495 52 - it('does not modify spec', () => { 496 + it('does not modify spec', async () => { 53 497 const spec: OpenApi.V3_1_X = { 54 498 ...specMetadataV3, 55 499 components: { ··· 61 505 }, 62 506 }; 63 507 64 - patchOpenApiSpec({ 508 + await patchOpenApiSpec({ 65 509 patchOptions: {}, 66 510 spec, 67 511 }); ··· 80 524 }); 81 525 82 526 describe('OpenAPI v3', () => { 83 - it('calls patch function', () => { 527 + it('calls patch function', async () => { 84 528 const fnBar = vi.fn(); 85 529 const fnFoo = vi.fn(); 86 530 ··· 98 542 }, 99 543 }; 100 544 101 - patchOpenApiSpec({ 545 + await patchOpenApiSpec({ 102 546 patchOptions: { 103 547 schemas: { 104 548 Bar: fnBar, ··· 118 562 }); 119 563 }); 120 564 121 - it('patch function mutates spec', () => { 565 + it('patch function mutates spec', async () => { 122 566 const spec: OpenApi.V3_1_X = { 123 567 ...specMetadataV3, 124 568 components: { ··· 162 606 }, 163 607 }; 164 608 165 - patchOpenApiSpec({ 609 + await patchOpenApiSpec({ 166 610 patchOptions: { 167 611 parameters: { 168 612 Foo: (schema) => { ··· 256 700 }); 257 701 }); 258 702 259 - it('handles spec without components', () => { 703 + it('handles spec without components', async () => { 260 704 const fn = vi.fn(); 261 705 262 706 const spec: OpenApi.V3_1_X = { 263 707 ...specMetadataV3, 264 708 }; 265 709 266 - patchOpenApiSpec({ 710 + await patchOpenApiSpec({ 267 711 patchOptions: { 268 712 parameters: { 269 713 Foo: fn, ··· 284 728 expect(fn).not.toHaveBeenCalled(); 285 729 }); 286 730 287 - it('handles spec without component namespaces', () => { 731 + it('handles spec without component namespaces', async () => { 288 732 const fn = vi.fn(); 289 733 290 734 const spec: OpenApi.V3_1_X = { ··· 292 736 components: {}, 293 737 }; 294 738 295 - patchOpenApiSpec({ 739 + await patchOpenApiSpec({ 296 740 patchOptions: { 297 741 parameters: { 298 742 Foo: fn, ··· 313 757 expect(fn).not.toHaveBeenCalled(); 314 758 }); 315 759 316 - it('handles spec without matching components', () => { 760 + it('handles spec without matching components', async () => { 317 761 const fn = vi.fn(); 318 762 319 763 const spec: OpenApi.V3_1_X = { ··· 326 770 }, 327 771 }; 328 772 329 - patchOpenApiSpec({ 773 + await patchOpenApiSpec({ 330 774 patchOptions: { 331 775 parameters: { 332 776 Foo: fn, ··· 347 791 expect(fn).not.toHaveBeenCalled(); 348 792 }); 349 793 350 - it('skips invalid schemas', () => { 794 + it('skips invalid schemas', async () => { 351 795 const fn = vi.fn(); 352 796 353 797 const spec: OpenApi.V3_1_X = { ··· 364 808 }, 365 809 }; 366 810 367 - patchOpenApiSpec({ 811 + await patchOpenApiSpec({ 368 812 patchOptions: { 369 813 schemas: { 370 814 Bar: fn, ··· 382 826 }); 383 827 }); 384 828 385 - it('applies meta patch function', () => { 829 + it('applies meta patch function', async () => { 386 830 const metaFn = vi.fn((meta) => { 387 831 meta.title = 'Changed Title'; 388 832 }); 389 833 const spec: OpenApi.V3_1_X = { 390 834 ...specMetadataV3, 391 835 }; 392 - patchOpenApiSpec({ 836 + await patchOpenApiSpec({ 393 837 patchOptions: { 394 838 meta: metaFn, 395 839 }, ··· 399 843 expect(spec.info.title).toBe('Changed Title'); 400 844 }); 401 845 402 - it('applies version patch function', () => { 846 + it('applies version patch function', async () => { 403 847 const versionFn = vi.fn((version) => `patched-${version}`); 404 848 const spec: OpenApi.V3_1_X = { 405 849 ...specMetadataV3, 406 850 }; 407 - patchOpenApiSpec({ 851 + await patchOpenApiSpec({ 408 852 patchOptions: { 409 853 version: versionFn, 410 854 }, ··· 416 860 }); 417 861 418 862 describe('OpenAPI v2', () => { 419 - it('calls patch function', () => { 863 + it('calls patch function', async () => { 420 864 const fnBar = vi.fn(); 421 865 const fnFoo = vi.fn(); 422 866 ··· 432 876 }, 433 877 }; 434 878 435 - patchOpenApiSpec({ 879 + await patchOpenApiSpec({ 436 880 patchOptions: { 437 881 schemas: { 438 882 Bar: fnBar, ··· 452 896 }); 453 897 }); 454 898 455 - it('patch function mutates schema', () => { 899 + it('patch function mutates schema', async () => { 456 900 const spec: OpenApi.V2_0_X = { 457 901 ...specMetadataV2, 458 902 definitions: { ··· 462 906 }, 463 907 }; 464 908 465 - patchOpenApiSpec({ 909 + await patchOpenApiSpec({ 466 910 patchOptions: { 467 911 schemas: { 468 912 Foo: (schema) => { ··· 483 927 }); 484 928 }); 485 929 486 - it('handles spec without definitions', () => { 930 + it('handles spec without definitions', async () => { 487 931 const fn = vi.fn(); 488 932 489 933 const spec: OpenApi.V2_0_X = { 490 934 ...specMetadataV2, 491 935 }; 492 936 493 - patchOpenApiSpec({ 937 + await patchOpenApiSpec({ 494 938 patchOptions: { 495 939 parameters: { 496 940 Foo: fn, ··· 511 955 expect(fn).not.toHaveBeenCalled(); 512 956 }); 513 957 514 - it('handles spec without matching definitions', () => { 958 + it('handles spec without matching definitions', async () => { 515 959 const fn = vi.fn(); 516 960 517 961 const spec: OpenApi.V2_0_X = { ··· 519 963 definitions: {}, 520 964 }; 521 965 522 - patchOpenApiSpec({ 966 + await patchOpenApiSpec({ 523 967 patchOptions: { 524 968 parameters: { 525 969 Foo: fn, ··· 540 984 expect(fn).not.toHaveBeenCalled(); 541 985 }); 542 986 543 - it('skips invalid schemas', () => { 987 + it('skips invalid schemas', async () => { 544 988 const fn = vi.fn(); 545 989 546 990 const spec: OpenApi.V2_0_X = { ··· 555 999 }, 556 1000 }; 557 1001 558 - patchOpenApiSpec({ 1002 + await patchOpenApiSpec({ 559 1003 patchOptions: { 560 1004 schemas: { 561 1005 Bar: fn, ··· 573 1017 }); 574 1018 }); 575 1019 576 - it('applies meta patch function', () => { 1020 + it('applies meta patch function', async () => { 577 1021 const metaFn = vi.fn((meta) => { 578 1022 meta.title = 'Changed Title'; 579 1023 }); 580 1024 const spec: OpenApi.V2_0_X = { 581 1025 ...specMetadataV2, 582 1026 }; 583 - patchOpenApiSpec({ 1027 + await patchOpenApiSpec({ 584 1028 patchOptions: { 585 1029 meta: metaFn, 586 1030 }, ··· 590 1034 expect(spec.info.title).toBe('Changed Title'); 591 1035 }); 592 1036 593 - it('applies version patch function', () => { 1037 + it('applies version patch function', async () => { 594 1038 const versionFn = vi.fn((version) => `patched-${version}`); 595 1039 const spec: OpenApi.V2_0_X = { 596 1040 ...specMetadataV2, 597 1041 }; 598 - patchOpenApiSpec({ 1042 + await patchOpenApiSpec({ 599 1043 patchOptions: { 600 1044 version: versionFn, 601 1045 }, ··· 607 1051 }); 608 1052 609 1053 describe('real-world usage', () => { 610 - it('handles complex schema example from docs', () => { 1054 + it('handles complex schema example from docs', async () => { 611 1055 const spec: OpenApi.V3_1_X = { 612 1056 ...specMetadataV3, 613 1057 components: { ··· 627 1071 }, 628 1072 }; 629 1073 630 - patchOpenApiSpec({ 1074 + await patchOpenApiSpec({ 631 1075 patchOptions: { 632 1076 schemas: { 633 1077 Foo: (schema: any) => { ··· 660 1104 }); 661 1105 }); 662 1106 663 - it('handles adding new schema properties', () => { 1107 + it('handles adding new schema properties', async () => { 664 1108 const spec: OpenApi.V3_1_X = { 665 1109 ...specMetadataV3, 666 1110 components: { ··· 675 1119 }, 676 1120 }; 677 1121 678 - patchOpenApiSpec({ 1122 + await patchOpenApiSpec({ 679 1123 patchOptions: { 680 1124 schemas: { 681 1125 Foo: (schema: any) => { ··· 710 1154 }); 711 1155 }); 712 1156 713 - it('handles removing schema properties', () => { 1157 + it('handles removing schema properties', async () => { 714 1158 const spec: OpenApi.V3_1_X = { 715 1159 ...specMetadataV3, 716 1160 components: { ··· 727 1171 }, 728 1172 }; 729 1173 730 - patchOpenApiSpec({ 1174 + await patchOpenApiSpec({ 731 1175 patchOptions: { 732 1176 schemas: { 733 1177 Foo: (schema: any) => {
+10 -1
packages/shared/src/openApi/shared/utils/patch.ts
··· 1 1 import type { Patch } from '../../../config/parser/patch'; 2 2 import type { OpenApi } from '../../../openApi/types'; 3 3 4 - export function patchOpenApiSpec({ 4 + export async function patchOpenApiSpec({ 5 5 patchOptions, 6 6 spec: _spec, 7 7 }: { ··· 13 13 } 14 14 15 15 const spec = _spec as OpenApi.V2_0_X | OpenApi.V3_0_X | OpenApi.V3_1_X; 16 + 17 + if (typeof patchOptions === 'function') { 18 + await patchOptions(spec); 19 + return; 20 + } 21 + 22 + if (patchOptions.input) { 23 + await patchOptions.input(spec); 24 + } 16 25 17 26 if ('swagger' in spec) { 18 27 if (patchOptions.version && spec.swagger) {